日本のSaaSスタートアップが世界で戦うためのプロダクトを開発するということ

こんにちは。昨年の11月にエンジニアとしてコミューンに入社したざびえる(仮名)です。

コミューンは、CEOのブログ「進出して分かった日本とアメリカのSaaSプロダクトニーズの違い|高田優哉/commmune|note」にもある通り、アメリカ進出をしており、私はその開発担当をしています。

この記事では、海外展開に関わるようになった経緯や、プロダクトのグローバル化をどのように進めているか、そして今後のグローバル化の課題などをお話しようと思います。

GlobalPJ参加の経緯

新卒でWebエンジニアとして働き始めてから常々「日本初のソフトウェアプロダクトで世界的に使われるものってないな。いつかそういうプロダクトが出て来れば日本のソフトウェア業界ももっと盛り上がって、優秀な人がソフトウェアエンジニアとして集まってくるだろうに」と思っていました。

一つ思い当たるのはプログラミング言語のRubyですが、ソフトウェアエンジニアのみぞ知るというやつで一般人には「日本のソフトウェアもやるな」という印象を与えられていないのが、惜しいところです。

そういう思いも密かに持ちつつ、転職活動でも海外展開を考えているサービスを提供している会社を探していました。 コミューンの入社面接のときに「海外展開に携われると嬉しい。海外展開の計画はあるか?」みたいな質問をしたところ、「具体的な計画はまだないが考えている。やるとしたら東アジアの可能性が高い」というような話を聞いていました。

新しい領域を切り開いているし、事業の成長性も群を抜いていると思ったので入社することに決めました。

そして、いざ入社してみると、入社初日に役員から呼び出され「アメリカ進出するので、開発の旗振り役お願いしたい。適任はざびえるさんしかいない」と言われました。 右も左も分からない状態で、幅が広くて抽象度の高い仕事が降ってきたなと一瞬ひるみつつ「わ…っかりました。やってみます!」と引き受けました。

海外展開のための開発

コミューンは今までクライアントもエンドユーザーも日本に住んでいる日本人のみと想定していたので、表示される言語は日本語のみ、時間(タイムゾーン)も日本標準時1つだけという前提でシステムが作られていました。

このプロダクトで海外に進出するためには、何らかの基準でUIの言語を切り替えたり時間の表記形式を切り替えたりする仕組みを追加しなければいけません。いわゆる多言語化、国際化(internationalization、略してi18n)というものです。

ライブラリの選定

コミューンのプロダクトはフロントもバックエンドもTypeScriptとJavaScriptで書かれています。 多言語化する上で、候補に上がったJavaScriptのライブラリは色々ありました。

世間での利用状況を見てみると、i18next系のライブラリが優勢だったのでそれに従うことにしました。

i18nextが多言語化のコアな機能を提供するライブラリで、 react-i18nextはi18nextをReactのコンポーネントから利用しやすくする機能を提供するライブラリです。 next-i18nextは、react-i18nextをさらにNext.jsで使いやすくする機能を提供しています。 i18next自体は、名前にnextと入っていますが、Next.jsとは関係ありません。

弊社のサービスはNext.jsで作られているので、next-i18nextを使えれば最適で自然な流れですが、 コミューンのコードには現在では非推奨とされているgetInitialPropsが残っており、それとの相性が悪いので使えませんでした。 next-i18nextのserverSideTranslationsはNext.jsのgetStaticPropsgetServerSideProps(サーバーサイドで動く)と組み合わせて利用するものであり、 getInitialProps(クライアントサイドで動く)と組み合わせられません。 参考:serverSideTranslationsのドキュメント

そこでフロントエンドではreact-i18nextを、サーバーサイドではi18nextを利用することにしました。

コミューンでのi18nの構成

対訳ファイルのディレクトリ構成

コミューンのUIはアトミックデザインで構成されています。画面要素はatomやmolecule、organism、templatesに分解されて、以下のようなファイルレイアウト(一部抜粋)になっています。

components/molecules/edit_modal
components/molecules/reaction
components/molecules/admin/settings/reaction
components/atoms/reaction
components/atoms/box_contents_parts
components/templates/edit_modal
components/templates/admin_push_notification
components/templates/admin_training
components/templates/admin_training/TrainingCategoriesContentContainer
components/templates/admin/knowledge_base
components/organisms/directmessage
components/organisms/admin/settings/reaction

というわけで、関係するテキストは1つのJSONファイルにまとめるという狙いで、

「翻訳対象のファイルが属しているディレクトリに相当するJSONファイルをtranslation/に置く」

という構成にすることにしました。

たとえば、翻訳対象のファイルが、

components/templates/admin_event/AddContainer.tsx

だとすると、そこに含まれるテキストの対訳データは

translation/components/templates/admin_event.json

というJSONファイルに入れることになります。

こうすることで、ディレクトリが機能別に分かれているので、ある機能に関する用語が1ファイルにまとまり管理しやすくなると期待できます。

i18nextでは、ネームスペースという仕組みで、対訳データJSONファイルの木構造の一部を取り出すことができます。 コンポーネントファイルから取り出すべき部分がわかりやすいように、そのディレクトリがネームスペースになるようにすることにしました。

import components_templates_admin_event from './translation/components/templates/admin_event.json'

const resources = {
  en: {
    'components/templates/admin_event': components_templates_admin_event.en.translation,
  },
  ja: {
    'components/templates/admin_event': components_templates_admin_event.ja.translation,
  }
}

対訳ファイルフォーマット

UIからは、言語ごとに文字列をキーにして紐づけられた対訳を取り出すことになりますが、このキーの作り方にはいくつかの流派があるようです。

  • メイン言語の表現をキーにする
  • テキストを定数化したときの定数名のようなキーにする
  • UIパーツの管理上のIDをキーにする

コミューンの多言語化では、基本的にはメイン言語の表現をキーにすることにしました。 翻訳者やUXライターがコンポーネントのコードから素直にコンテキストとテキストの関係を理解できるようにするためです。

対訳ファイルは以下のような構成にしました。

{
  "en": {
    "translation": {
      "Save": "Save"
    }
  },
  "ja": {
    "translation": {
      "Save": "保存する"
    }
  }
}

日本語と英語を別ファイルにしたり、"translation"のネストを無くしたりすることも可能ですが、バックエンドで使うライブラリi18nextのフォーマットに合わせるためにこうすることにしました。

対訳データを利用するコード

対訳データを利用するコードでは、以下のようにi18nextライブラリのt()関数にキーを渡して対訳を取り出します。

import { useTranslation } from 'react-i18next'

// ネームスペースを指定して、対応する対訳データをtから使えるようにする
const { t } = useTranslation(['pages/admin/event'])

// tを使って対訳データを取り出す
<ColorButton>
  {t('Save')}
</ColorButton>

クラスコンポーネントでは、useTranslationはhookなので使えず、代わりにwithTranslationというHOCを使います。

翻訳者との連携

コミューンの開発チームはVSCodeを使うことになっています(強制ではなくIntelliJを使っている人もいます)。

本来であれば、t()の埋め込みはエンジニアが行う作業だと思われますが、翻訳者さんがJavaScriptやVSCode、git、GitHubなどに理解のある方だったので、チームメンバーの才能を信じて色々とお任せしてみることにしました。

翻訳者さんは、

  • JavaScript: コメントと文字列を見分けるぐらいには文法が分かる
  • VSCode: 自分でチートシートを読んで使い方を覚えるぐらいはできる

ということだったので、gitとGitHubの操作(status/add/commit/push/pull/merge)やVSCodeで翻訳作業に便利そうな機能を色々を教えて、

  • 翻訳
  • 対訳データJSON作成
  • t()の埋め込み
  • メインブランチの更新をマージして取り込む
  • 直接コードに翻訳結果を書いてもらいコミット

の作業をお願いすることにしました。また、翻訳者さんがVSCodeで行う作業の効率アップためにスニペットを作ってあげました。

これでエンジニアが後でやらなければいけない作業を翻訳作業に混ぜ込めたし、作業の引き継ぎもリポジトリのデータ同士なのでスムーズとなり、全体のプロセスとしてはかなり効率アップしたと言えます。

VSCode拡張

さて、多言語化の方針が決まり、自分の他に三人のエンジニアが多言語化の作業に参加してくれることになりました。

膨大な数のファイルで定型作業を行うので、VS Codeの拡張を作って作業効率化をすることにしました。

Your First Extension | Visual Studio Code Extension APIを参考に開発をしました。

機能としては、

  • 編集中のコンポーネントに対応する対訳データJSONファイルを開く
  • 編集中のコンポーネントに対応する対訳データファイルのパスにフォーマット通りのJSONでファイルを作成する
  • 関数コンポーネントに対して、useTranslationの呼び出しとインポートを半自動挿入する
  • クラスコンポーネントに対して、withTranslationの呼び出しとインポートを半自動挿入する

などのコマンドを作成しました。

JSXを返すだけの関数コンポーネントの場合は、

=> (
  <>
  </>
)

のようになっていますが、useTranslationの呼び出しを挿入するためにはそもそも

=> {
  return (
    <>
    </>
  )
}

のように書き換えなければいけません。拡張ではこのような場合にも対応できるように作りました。

こういったことができるのもコンポーネントファイルと対訳ファイル、ネームスペースの関係に一貫性を持たせたおかげと言えます。

これらのおかげでエンジニアは定型作業をする必要がほぼなくなり、エンジニアリングとして本質的な部分に注力できるようになりました。

実装の細かい話

さてここで細かい実装上の工夫も紹介したいと思います。

例えば、次のようなものがあります。

まず、英語では、単数/複数によってテキストを切り替える必要が出てきます。これ自体はi18nextにそのための機能があります。

以下のように対訳データに_one_otherをつけておくと、countが1かそれ以外かで取り出すものが切り替わります。

t('{{count}} person', {count: number})
{
  "en": {
    "{{count}} person_one": "{{count}} person",
    "{{count}} person_other": "{{count}} people"
  },
  "ja": {
    "{{count}} person": "{{count}}人"
  }
}

「人」だけを翻訳対象にしてcountはコンポーネント側のコードにすることも可能ですが、スペースのありなし調整などが厄介なのでこの形式にしました。

単純に対訳データを作ると重複したキーになる (英語テキストは同じだが日本語テキストが異なる) 場合は、「英語テキスト _SUFFIX」という形式で一意のキーにします。

{
  "en": {
    "translation": {
      "Delete_MODAL": "Delete",
      "Delete_MENU": "Delete"
    }
  },
  "ja": {
    "translation": {
      "Delete_MODAL": "削除する",
      "Delete_MENU": "削除"
    }
  }
}

そういう形式にした理由は、画面に表示される文字列からソース検索するときに

t('Delete

とやりやすくするためです。

こういった点は、作業者やファイルによってバラバラな書き方になってしまうと将来の保守性が悪くなってしまいます。

これらも含めて実装中に気づいたりレビューをする上で気になった点は、 業務委託で実装の手伝いをして頂いているhiro08さんがまとめてくれていて、 後々の実装やレビューでも参照できるようになっています。 こちらは機会があればまた別の記事で紹介しようと思います。

反省

言語の切り替えは概ね完了し7/1に英語版をローンチすることができいくつかの会社がトライアルで利用してくれることになりましたが、 開発チームとしてはまだまだやりきれていないこともたくさんあります。こうしていればもっとうまくできたであろうと思う点もいくつかあります。

まずは、今後プロダクトを多言語化する方向けのアドバイスも兼ねて反省点を書きます。

  • 日本語の表記揺れやネーミングの不統一を先に解消した方が良い
    • これが先に解消されていると翻訳が簡単になり、後から解消しようとすると対訳データの作り直しが大変です
  • 日本語テキストの重複とその元になっているコードの重複を減らせるだけ減らしておいた方が良い
    • 多言語化はコード修正が割と単純で確認作業に割く時間の方が多いですが、重複があるとその分の時間が無駄になってしまいます
  • 使われていないコードを消しておいた方が良い
    • 消していないと無駄な翻訳や多言語化作業で時間を消費するし、将来の保守対象(負の遺産)も増えてしまいます
  • 日本語テキストをシンプルにした方が良い
    • 主語と動詞が別ファイルにあったり別々の変数になったりしていて、それを関数で繋げていると翻訳が困難です
  • 新規開発を多言語化対応込みでやってもらうのをプロジェクト開始時点からやった方が良い
    • 翻訳漏れのチェックや対応がとても面倒になります

グローバル化の課題

最後に、プロダクトのグローバル化は、単に言語が英語などで表示できるようにするだけでなく、以下のような作り替えも必要になります。

  • 表示領域の調整(一般的に英語で書いた方が長くなる)
  • 入力文字数や文字種制限の調整
  • 日時の表示形式の切り替え
  • タイムゾーンに合わせた時刻の表示
  • タイムゾーンを考慮したバッチの起動

また、海外のユーザーには受け入れ難い日本の深夜に行っているシステムメンテナンスを改めたり、サポートを英語化したり、GDPR対応もしなければいけません。

日本のクライアントのニーズに応えつつ大幅に異なるグローバルのニーズにも合うように機能の追加や修正もしなければいけません。

後書き

この記事が少しでも、日本発のITプロダクトを世界で羽ばたかせるべく頑張っている方々の参考に、そしてこれから海外展開を考えている人の後押しになれば幸いです。

コミューン開発チームも、プロダクトが世界で通用するよう、今後も開発プロセスやアーキテクチャーの改善、組織づくりを引き続き頑張ります。 一緒に頑張って頂ける熱意とスキルをもったエンジニアを募集しています。興味のある方は是非以下のページからカジュアル面談の申し込みをしてみて下さい。

https://meety.net/articles/t2--dw3b0wqu6vcmeety.net

commmune-careers.studio.site