共有文字列

複数のコンポーネントやファイルで使われる文字列を国際化する方法

共有文字列は、アプリケーション内の複数の箇所で使われるテキスト値です。例として、ナビゲーションラベル、フォームメッセージ、設定データなどが挙げられます。各所で翻訳処理を重複させるのではなく、msgで文字列を翻訳対象としてマークし、useMessagesでその文字列をデコードしてください。

共有コンテンツの課題

アプリ全体で共通して使われる次のナビゲーション設定を考えてみましょう:

// navData.ts
export const navData = [
  {
    label: 'ホーム',
    description: 'ホームページ',
    href: '/'
  },
  {
    label: '会社概要', 
    description: '会社情報',
    href: '/about'
  }
];

これを国際化するには、通常は次を行います。

  1. 翻訳関数を受け取る関数に書き換える
  2. すべての使用箇所を、t を渡してその関数を呼ぶように更新する
  3. コードベース全体の複雑さを管理する

これは保守コストを増やし、コードの可読性を損ないます。msg 関数は、翻訳対象の文字列にその場でマークを付け、必要なときにデコードできるようにすることで、これを解決します。

クイックスタート

文字列のマーキングにはmsgを、デコードにはuseMessagesを使用します。

// navData.ts - 翻訳対象の文字列をマーク
import { msg } from 'gt-next';

export const navData = [
  {
    label: msg('ホーム'),
    description: msg('ホームページ'), 
    href: '/'
  },
  {
    label: msg('会社概要'),
    description: msg('会社に関する情報'),
    href: '/about'
  }
];
// コンポーネントの使用 - マークされた文字列のデコード
import { useMessages } from 'gt-next';
import { navData } from './navData';

function Navigation() {
  const m = useMessages();
  
  return (
    <nav>
      {navData.map((item) => (
        <a key={item.href} href={item.href} title={m(item.description)}>
          {m(item.label)}
        </a>
      ))}
    </nav>
  );
}

共有文字列の仕組み

共有文字列システムは2つのフェーズで動作します。

  1. マークフェーズ: msg が文字列に翻訳メタデータをエンコードします
  2. デコードフェーズ: useMessages または getMessages が文字列をデコードして翻訳します
// msg() は文字列をメタデータ付きでエンコードします
const encoded = msg('Hello, world!');
console.log(encoded); // "Hello, world!:eyIkX2hhc2giOiJkMjA3MDliZGExNjNlZmM2In0="

// useMessages() はデコードして翻訳します
const m = useMessages();
const translated = m(encoded); // ユーザーの言語での「Hello, world!」

msg が返す encoded string は直接使用できません。必ず useMessages または getMessages でデコードしてください。

クライアントとサーバーの使い分け

クライアントコンポーネント

useMessages フックを使います。

import { useMessages } from 'gt-next';

const encodedString = msg('Hello, world!');

function MyComponent() {
  const m = useMessages();
  return <div>{m(encodedString)}</div>;
}

Server Components

getMessages 関数を使用してください:

import { getMessages } from 'gt-next/server';

const encodedString = msg('こんにちは、世界!');

async function MyServerComponent() {
  const m = await getMessages();
  return <div>{m(encodedString)}</div>;
}

decodeMsg で元の文字列を取得する

ログ出力やデバッグ、比較など、翻訳せずに元の文字列にアクセスしたい場合があります。decodeMsg を使うと、元のテキストを取り出せます。

import { decodeMsg } from 'gt-next';

const encoded = msg('Hello, world!');
const original = decodeMsg(encoded); // "Hello, world!" (元の文字列)
const translated = m(encoded); // "Hello, world!" (ユーザーの言語で表示)

// ログ出力やデバッグに便利
console.log('元の文字列:', decodeMsg(encoded));
console.log('翻訳後の文字列:', m(encoded));

decodeMsg のユースケース

  • 開発・デバッグ: トラブルシューティングのために原文文字列をログ出力
  • フォールバック処理: 翻訳に失敗した場合は原文テキストを使用
  • 文字列比較: 既知の原文の値と比較
  • アナリティクス: 原文文字列の利用状況を追跡
// 例: フォールバック処理
function getDisplayText(encodedStr) {
  const m = useMessages();
  try {
    return m(encodedStr);
  } catch (error) {
    console.warn('翻訳に失敗しました。元のテキストを使用します:', decodeMsg(encodedStr));
    return decodeMsg(encodedStr);
  }
}

Variable の使用

動的な内容を含む文字列にはプレースホルダーを使用し、variables を渡します。

// Mark string with variables
const items = 100;
export const pricing = [
  {
    name: 'ベーシック',
    price: 100,
    description: msg('ベーシックプランには{items}件のアイテムが含まれます', { items })
  }
];
// コンポーネントで使用
function PricingCard() {
  const m = useMessages();
  
  return (
    <div>
      <h3>{pricing[0].name}</h3>
      <p>{m(pricing[0].description)}</p>
    </div>
  );
}

ICUメッセージフォーマット

高度な書式設定には、ICU の構文を使用します。

const count = 10;
const message = msg('カートに{count, plural, =0 {商品はありません} =1 {商品が1点あります} other {商品が{count}点あります}}', { count });

ICU Message Format の詳細はUnicode のドキュメントをご覧ください。

ナビゲーションの設定

// config/navigation.ts
import { msg } from 'gt-next';

export const mainNav = [
  {
    label: msg('ホーム'),
    href: '/',
    icon: 'home'
  },
  {
    label: msg('製品'),
    href: '/products', 
    icon: 'package'
  },
  {
    label: msg('私たちについて'),
    href: '/about',
    icon: 'info'
  }
];

export const footerLinks = [
  {
    title: msg('企業情報'),
    links: [
      { label: msg('私たちについて'), href: '/about' },
      { label: msg('採用情報'), href: '/careers' },
      { label: msg('お問い合わせ'), href: '/contact' }
    ]
  },
  {
    title: msg('サポート'), 
    links: [
      { label: msg('ヘルプセンター'), href: '/help' },
      { label: msg('ドキュメント'), href: '/docs' },
      { label: msg('APIリファレンス'), href: '/api' }
    ]
  }
];
// components/Navigation.tsx
import { useMessages } from 'gt-next';
import { mainNav } from '../config/navigation';

function Navigation() {
  const m = useMessages();
  
  return (
    <nav>
      {mainNav.map((item) => (
        <a key={item.href} href={item.href}>
          <Icon name={item.icon} />
          {m(item.label)}
        </a>
      ))}
    </nav>
  );
}

フォーム構成

// config/forms.ts
import { msg } from 'gt-next';

export const formMessages = {
  placeholders: {
    email: msg('メールアドレスを入力'),
    password: msg('パスワードを入力'),
    message: msg('メッセージを入力...')
  },
  actions: {
    send: msg('送信'),
    save: msg('保存'),
    cancel: msg('キャンセル')
  },
  validation: {
    required: msg('必須項目です'),
    email: msg('有効なメールアドレスを入力してください'),
    minLength: msg('{min}文字以上で入力してください', { min: 8 }),
    maxLength: msg('{max}文字以内で入力してください', { max: 100 })
  },
  success: {
    saved: msg('変更を保存しました'),
    sent: msg('メッセージを送信しました'),
    updated: msg('プロフィールを更新しました')
  },
  errors: {
    network: msg('ネットワークエラーが発生しました。もう一度お試しください'),
    server: msg('サーバーエラーが発生しました。サポートにお問い合わせください'),
    timeout: msg('タイムアウトしました。もう一度お試しください')
  }
};
// components/ContactForm.tsx
import { useMessages } from 'gt-next';
import { formMessages } from '../config/forms';

function ContactForm() {
  const m = useMessages();
  const [errors, setErrors] = useState({});
  
  return (
    <form>
      <input 
        type="email"
        placeholder={m(formMessages.placeholders.email)}
        required
      />
      {errors.email && <span>{m(formMessages.validation.email)}</span>}
      
      <button type="submit">
        {m(formMessages.actions.send)}
      </button>
    </form>
  );
}

動的コンテンツの生成

// utils/productData.ts
import { msg } from 'gt-next';

function mockProducts() {
  return [
    { name: 'iPhone 15', company: 'Apple', category: 'Electronics' },
    { name: 'Galaxy S24', company: 'Samsung', category: 'Electronics' }
  ];
}

export function getProductData() {
  const products = mockProducts();
  
  return products.map(product => ({
    ...product,
    description: msg('{name}は{company}の{category}製品です', {
      name: product.name,
      category: product.category,
      company: product.company
    })
  }));
}
// components/ProductList.tsx
import { useMessages } from 'gt-next';
import { getProductData } from '../utils/productData';

function ProductList() {
  const m = useMessages();
  const products = getProductData();
  
  return (
    <div>
      {products.map(product => (
        <div key={product.name}>
          <h3>{product.name}</h3>
          <p>{m(product.description)}</p>
        </div>
      ))}
    </div>
  );
}

よくある問題

エンコード済み文字列を直接使う

msg の出力を直接使わないでください。

// ❌ 誤り - encoded string を直接使用している
const encoded = msg('Hello, world!');
return <div>{encoded}</div>; // 翻訳ではなく encoded string が表示される

// ✅ 正解 - 先に文字列をデコードする
const encoded = msg('Hello, world!');
const m = useMessages();
return <div>{m(encoded)}</div>; // 正しく翻訳が表示される

msg() の動的コンテンツ

文字列はビルド時点で既知である必要があります:

// ❌ 誤り - 動的テンプレートリテラル
const name = 'John';
const message = msg(`Hello, ${name}`); // ビルド時エラー

// ✅ 正しい - variablesを使用
const name = 'John';
const message = msg('Hello, {name}', { name });

デコードのし忘れ

すべてのmsg文字列はデコードが必要です:

// ❌ デコードが不足している
const config = {
  title: msg('ダッシュボード'),
  subtitle: msg('お帰りなさい')
};

// コンポーネント内で後ほど - デコードを忘れた場合
return <h1>{config.title}</h1>; // エンコードされた文字列が表示される

// ✅ 正しい方法 - 使用時にデコードする
const m = useMessages();
return <h1>{m(config.title)}</h1>; // 翻訳されたタイトルが表示される

次のステップ

このガイドはいかがですか?