🌐

Rails/Laravelのi18nをReact/Vueで使いたくてライブラリを勉強がてら作ってみた

に公開
2026/01/02

はじめに

Rails や Laravel でバックエンドを作って、フロントは React や Vue で SPA にする構成はよくあると思います。

そういうときに翻訳ファイルをどうするか問題がありまして、バックエンドの config/locales/*.yml とフロントの翻訳ファイル、両方管理するのはしんどい。できればバックエンドのやつをそのまま使いたい。

そして、個人的にはプロジェクトのマイグレーション等で使えるように、できればそれぞれの組み合わせに互換性のあるものがほしかったのですが、既存のライブラリを調べたもののよさそうなものが無かったので、勉強がてら自分で作ってみました。

作成したものはこちら:

https://github.com/usapopopooon/bf-i18n

既存ライブラリを調べてみた

Rails + JavaScript 系

i18n-js (fnando)

Rails の YAML をエクスポートして JavaScript で使うやつ。gem と npm パッケージ両方あります。

https://www.npmjs.com/package/i18n-js

これ自体は良さそうなんですが、React や Vue の hooks / composables がなくて、自分でラップする必要がありそうでした。

@shopify/react-i18n(deprecated)

Shopify が作った React 用の i18n ライブラリ。Rails の i18n と一貫性のある API を提供しているとのこと。

https://www.npmjs.com/package/@shopify/react-i18n

ただ Shopify 向けに最適化されていて、Laravel には対応していません。さらに、現在は deprecated になっています。

Laravel + React

laravel-react-i18n

Laravel + React 専用のライブラリ。Vite プラグインもあって、useLaravelReactI18n という hooks も用意されています。

https://www.npmjs.com/package/laravel-react-i18n

これはかなり良さそうでした。ただ Laravel 専用なので、Rails では使えません。

Laravel + Vue

laravel-vue-i18n

Laravel + Vue3 専用のライブラリ。こちらも Vite プラグインあり、composables あり。

https://www.npmjs.com/package/laravel-vue-i18n

これも良さそうなんですが、Laravel + Vue の組み合わせ限定です。

まとめ

調べてみると、個別の組み合わせに対応したライブラリはそれぞれあります。

組み合わせ ライブラリ
Rails + React i18n-js + 自前ラップ、@shopify/react-i18n(deprecated)
Laravel + React laravel-react-i18n
Laravel + Vue laravel-vue-i18n
Rails + Vue 見つけられませんでした(もしあったらすみません)

ただ、Rails/Laravel 両方に対応していて、かつ React/Vue 両方で同じ API で使えるものは見つかりませんでした。

作成したライブラリの概要

bf-i18n という名前で npm に公開しました。bf は backend-friendly の略です。

特徴としては:

  • Rails 形式でも Laravel 形式でも、バックエンドの翻訳ファイルをそのままフロントで使える
  • React と Vue で同じ API なので、フレームワーク移行時も学習コストが低い
  • hooks / composables が最初から用意されているので、自前でラップする必要がない
  • モノレポ構成なので、必要なパッケージだけインストールできる

パッケージ構成

モノレポで4つに分けました。

パッケージ 説明
@bf-i18n/core コア機能
@bf-i18n/react React 用
@bf-i18n/vue Vue 用
@bf-i18n/cli CLI

使い方

対応モード

Rails 形式でも Laravel 形式でも、どちらをベースにしていてもフロントエンドで使えるようにしました。mode で切り替えます。

Rails モード

ja:
  greeting: "こんにちは、%{name}さん!"
  items:
    zero: "アイテムなし"
    one: "1個のアイテム"
    other: "%{count}個のアイテム"

Laravel モード

return [
    'greeting' => 'こんにちは、:nameさん!',
    'items' => '{0} アイテムなし|{1} 1個のアイテム|[2,*] :count個のアイテム',
];

基本

import { createI18n } from '@bf-i18n/core';

const i18n = createI18n({
  defaultLocale: 'ja',
  mode: 'rails',
  translations: {
    ja: {
      greeting: 'こんにちは、%{name}さん!',
    },
    en: {
      greeting: 'Hello, %{name}!',
    },
  },
});

i18n.t('greeting', { name: '世界' }); // "こんにちは、世界さん!"

React

import { I18nProvider, useTranslation } from '@bf-i18n/react';

function MyComponent() {
  const { t, setLocale } = useTranslation();

  return (
    <div>
      <p>{t('greeting', { name: 'React' })}</p>
      <button onClick={() => setLocale('en')}>English</button>
    </div>
  );
}

Vue

<script setup>
import { useTranslation } from '@bf-i18n/vue';

const { t, setLocale } = useTranslation();
</script>

<template>
  <p>{{ t('greeting', { name: 'Vue' }) }}</p>
  <button @click="setLocale('en')">English</button>
</template>

React と Vue で API を揃えています。

ブラウザロケール検出

navigator.languages から言語を自動で取得します。

const i18n = createI18n({
  defaultLocale: 'en',
  detectBrowserLocale: true,
  translations: { /* ... */ },
});

Missing Key トラッキング

存在しないキーを呼んだときに記録しておく機能。開発中に翻訳の追加忘れに気づけます。

i18n.t('missing.key'); // キーがないと "missing.key" がそのまま返る

if (i18n.hasMissingKeys()) {
  console.warn(i18n.getMissingKeys());
}

CLI の extract

ソースコードから t('key') を抽出する機能。正規表現でやっているので、動的にキーを組み立てているケースは拾えません。

bf-i18n extract ./src --output keys.json

内部処理の流れ

core パッケージは4つのクラスで構成されています。

I18n(メイン)
├── Translator(キーの解決、フォールバック)
├── Interpolator(変数の補間)
└── Pluralizer(複数形の処理)

補間処理

Rails 形式と Laravel 形式で変数のプレースホルダーが違うので、正規表現を動的に組み立てています。

// interpolator.ts
private buildPattern(): RegExp {
  const { prefix, suffix } = this.options;

  if (suffix) {
    // Rails形式: %{variable}
    const escapedPrefix = this.escapeRegExp(prefix);
    const escapedSuffix = this.escapeRegExp(suffix);
    return new RegExp(`${escapedPrefix}([a-zA-Z_][a-zA-Z0-9_]*)${escapedSuffix}`, 'g');
  } else {
    // Laravel形式: :variable
    const escapedPrefix = this.escapeRegExp(prefix);
    return new RegExp(`${escapedPrefix}([a-zA-Z_][a-zA-Z0-9_]*)`, 'g');
  }
}

複数形の処理

Rails は zero / one / other のキーで分ける形式、Laravel は {0} None|{1} One|[2,*] Many のパイプ区切り形式。どちらも Intl.PluralRules を使ってロケールに応じた複数形カテゴリを取得しています。

// pluralizer.ts
resolveKeyBased(
  translations: Record<string, TranslationValue>,
  count: number
): TranslationValue | undefined {
  // count=0 の場合は zero キーを優先(Rails互換)
  if (count === 0 && 'zero' in translations) {
    return translations['zero'];
  }

  const category = this.getPluralCategory(count) as PluralCategory;

  if (category in translations) {
    return translations[category];
  }

  // フォールバック: other
  if ('other' in translations) {
    return translations['other'];
  }

  return undefined;
}

React の再レンダリング

ロケールが変わったら再レンダリングしてほしいので、useSyncExternalStore を使っています。I18n クラスの onChange で購読して、ロケールが変わったらコンポーネントが更新されます。

// hooks.ts
export function useTranslation(options: UseTranslationOptions = {}): UseTranslationReturn {
  const i18n = useI18n();

  const locale = useSyncExternalStore(
    (callback) => i18n.onChange(callback),
    () => i18n.locale,
    () => i18n.locale
  );

  // ...
}

今後

Spring や Django の形式にも対応できたらとは思っています。ただ Rails/Laravel とだいぶ違うので、ちゃんとできるかはわかりません。

Discussion