再帰プログラミングを活用して、変更箇所を最小限に抑えたコンテンツの多言語対応を実施しました

f:id:aidemy-blog:20220217145552p:plain

目的

以前から、日本語のコンテンツを英語に自動翻訳して提供する仕組み(外部サービスに依存)がありましたが、より意図通りに翻訳したコンテンツを出せるようにするために、元のコンテンツに言語別の情報を持たせて出し分けられるようにする改修を実施しました。

同時並行で、コンテンツ作成ツール自体のリファクタリング(コード改修、ビルド工程見直し)も実施することで、コンテンツ作成ツールの保守性・拡張性を高めるとともに、コンテンツ作成の生産性を高めることを目指しました。

方針

最小工数かつ安全な実施を最優先に、コンテンツ作成ツール、バックエンドそれぞれで改修を実施しています。

コンテンツ作成ツールの改修

既存のコンテンツ作成ツールは、多言語対応しない(日本語情報のみの)コンテンツを生成していましたが、今回は多言語の情報を持ったコンテンツを生成してDBに挿入するように改修しています。

コンテンツに言語別の情報を持たせるためには、コンテンツのテンプレートデータ構造を定義・生成した上で、テンプレートオブジェクト内を再帰的に走査して言語別の情報が必要なパラメータ(ex: コンテンツのタイトル、テキスト等)に日本語・英語・...と言語ごとに情報を追記することで、多言語情報を持ったオブジェクトとしてMongoDBに登録することとします。

バックエンドの改修

バックエンド側は上記の逆で、MongoDBから取得した多言語対応版コンテンツ情報の中身を再帰的に走査し、あらかじめ指定された一言語のみの情報を持つコンテンツ情報のみにデコードすることで、従前と同じデータ構造のオブジェクトを使いまわせるようにします。

まとめ

再帰を使うことで、コンテンツ作成ツール・バックエンド側それぞれ十数行程度のコードの組み合わせにて簡潔に実装されます。DBの入出力周り(コンテンツ作成ツール、Repository)で多言語対応の面倒事を全て巻き取り切ったため、フロントエンドと(DB取得以外の)バックエンドのビジネスロジックにおいてほとんど変更が出ない(変更があったことを知らなくてよい)形です。

実装の手順

多言語対応が必要なパラメータに適用する型を定義します。以下はコードの一部で、kind: LANGUAGE を持っていれば国際化対応パラメータとして扱わせたいための型です。

export const LANGUAGE = 'LANGUAGE' as const;
export interface ILang<T extends string | string[]> {
  kind: typeof LANGUAGE;
  data: {
    lang: Lang;
    text: T;
  }[];
}

コンテンツ作成元データ(CSV, Jupyterファイル等)から、国際化対応したドキュメントを生成して、Mongoに挿入します。この手順自体は従前を踏襲していますが、CSVおよびJupyterファイルは別言語版をあらかじめ用意してあります。

以下は型情報を頼りにして、再帰的にテンプレートを走査して言語別情報を詰め込むソースコード(の一部)です。

const rec = (lang, source, template) => {
  if (source == null) return source;
  if (template && template['kind'] === LANGUAGE) {
    template['data'].push({ lang, text: source });
    return template;
  }
  if (typeof source !== 'object' || !Object.keys(source).length) return source;
  if (Array.isArray(source)) return source.filter(elm => elm !== undefined && elm !== null).map((elm, i) => rec(lang, elm, deepcopy(template[i] || template[0])));
  return Object.fromEntries(Object.keys(source).map(key => [key, rec(lang, source[key], template[key])]));
}

※ バックエンド側はほぼ上記の逆なので、割愛します

意義

シンプルな再帰実装を使う方針が奏功し、多言語対応自体はほぼ最小工数で要件を達成して安全なリリースを実現できました。加えて、コンテンツ作成チームからの要望を取り込みながらの改修作業、仕様のヒアリングと紐解き・再構築、安全なリリースに向けた手順組んだ結果、多言語対応自体の外でも価値が実現されています。

例えば以下の通りです。

  • コンテンツ部の生産性向上
    • CSVの整理: コース作成の元データとなるCSV情報を仕様ヒアリングもしながら整理整頓し、過去に発生していた編集・更新上のトラブルを回避するために、Google Spread Sheet上で履歴を残しながら安全にCSVエクスポートできる仕組みを導入
    • 一修正・変更毎のデプロイ時間の短縮(10分→2分30秒)
    • コンテンツの種類ごとにGithubのレポジトリが分散していたため集約したことで、管理コストを削減
    • 一部のJupyterファイルへの依存を解消、メンテナンス不要に
  • 新しいコンテンツ作成ツールに向けた先行投資
    • 「お客様により速く高品質なコンテンツを提供できるように、効果的にコンテンツを作成・編集できる新ツールを作成したい」という機運が社内にあり、実現にむけた布石となりました。
    • 現行コンテンツ作成ツールのリファクタリング(運用保守性、拡張性の改善)
      • 責務が拡大した中でも総コード行数は増えずに大きく減少しました。
      • 色々やっていたファイル(1,523行)がなくなり、ID生成ファイル(95行)、course作成ファイル(244行)、exercise作成ファイル(236行)に分割されています。
      • 責務分解、変数持ち回しの解消と宣言的記述、Single Source of Truth