ymmooot

実行時にファイルを読み込むのをやめる

実行時にファイルを読み込むタイプのライブラリがある。
例えば Vue I18n というフロントエンドの i18n ライブラリは以下のように利用できる。

en: 
  message: 
    hello: 'Hello {name}'
    welcome: 'Welcome'
jp: 
  message: 
    hello: 'こんにちは {name}'
    welcome: 'ようこそ'
<p>{{ $t('message.hello', { name: 'Bob' }) }}</p>

このタイプのライブラリは型安全ではないという大きすぎる欠点を持つ。
message.hello というキーが本当に翻訳ファイルに含まれているか型情報では分からないし、 name という引数を要求してることも分からない。

他にも Go 言語の text/template パッケージは以下のように利用できる。

package main

import (
    "log"
    "os"
    "text/template"
)

type Params struct {
    Name string
    Age  int
}

func main() {
    // 実際には外部ファイルを読み込んだりすることが多い
    tmpl := "My name is {{.Name}}. I am {{.Age}} years old."

    t, err := template.New("introduce").Parse(tmpl)
    if err != nil {
        log.Fatal(err)
    }
    params := Params{
        Name: "Bob",
        Age:  20,
    }
    if err := t.Execute(os.Stdout, params); err != nil {
        log.Fatal(err)
    }
}

この例でもテンプレート側の文字列を変更したときに、t.Execute が失敗するかどうかは実行時までわからない。

解決策

ではどうするか。
そのシステムのプログラミング言語のコードを事前に生成すれば良い。上述した Vue I18n なら以下のようになる。

// $tt は生成された typed t
$tt.message.hello({ name: 'bob' }); // 'Hello bob' or 'こんにちは bob'
$tt.message.welcome; // 'Welcome' or 'ようこそ'

あらかじめテンプレートとなるファイルをパースし、型安全にアクセスできる TypeScript のコードを生成すれば良い。
flutter では flutter_localizationsslang といったライブラリがこのようなコード生成を行っている。
事前のコード生成であれば、実行時ではなくビルド時にエラーに気づけるだけでなく、エディターの補完も利用でき開発効率もあがる。

自分はこの ttnuxt-i18n と以下のように連動させて利用している。

/plugins/typedt.tsexport default defineNuxtPlugin(nuxt => {
  const tt = createTT({
    initialLocale: nuxt.vueApp.$nuxt.$i18n.locale.value,
  });  
  nuxt.hook('i18n:localeSwitched', ({ newLocale }) => {
    tt.locale = newLocale;
  });
  nuxt.provide('tt', tt);
});

declare module '#app' {
  interface NuxtApp {
    $tt: typeof tt;
  }
}

declare module "@vue/runtime-core" {
  interface ComponentCustomProperties {
    $tt: typeof tt;
  }
}

なお、POEditor などの SaaS で翻訳ファイルを作成している場合、それを元にコード生成すると開発側が翻訳作業者に依存することになるが、これは正しくないように感じる。
必要な翻訳の Term は開発者にしか分からず、翻訳を担当するメンバーへの翻訳の依頼が発生してしまう。そこで、翻訳ファイルをスキーマとして依存性を逆転させる。

  1. 開発者は翻訳ファイルに新たに必要になった Term を追加する (value は空でよい)
  2. Term を翻訳ファイルから抜き出し API で POEditor に sync させ、翻訳者に通知を送る
  3. 翻訳ファイルをコードに変換する
  4. 翻訳者は追加された Term に対して翻訳を入れる
  5. POEditor の GitHub 連携で翻訳ファイルを更新する
  6. 翻訳ファイルをコードに変換する (新しい value が使えるようになる)

ステップ3と6は自動化できるため、都度都度意識して作業する必要はない。
このサイクルではステップ2が完了した時点で開発者と翻訳者ともに並列して作業が可能になる。こうすることで、翻訳者は開発者のキー追加に従って翻訳を入れるだけでよくなり、開発者は翻訳者に依存することなく開発を進められる。

他にも

このコード生成の考えはファイル以外でも同じで、そのシステムの型とは違う世界へのアクセス時にコード生成が有効である。
TypeScript のクライアントから GraphQL を扱う際は GraphQL Code Generator でスキーマやクエリを元にコードを生成することで、API 呼び出しを型安全にできる。
サーバーから DB へのアクセスも sqlc, jooq などのツールで DB の情報を予めプログラムに変換できる。
SendGrid などのメール配信サービスでテンプレートを SaaS 側に保存する場合でも、事前に API でテンプレートを取得し、テンプレート名と必要なパラメーターのセットをコードに変換することで型安全にアクセスできる。このパターンでは、開発者が SendGrid の管理画面をいちいち開く必要がなくなる。(SendGrid は GUI でのテンプレート変更の Webhook などがないため、それによる壊れを瞬時に検知することはできないが、少なくともメール送信のコードを書いてる時には型安全である)