ミニショップを国際化する
GT React のコンポーネント、フック、共有文字列を使ってシンプルなショップを国際化する実践的な React チュートリアル
外部サービスなし、ルーティングなし、UI フレームワークなしで、小さなローカル完結の「ミニショップ」を起動し、翻訳まで行います。GT React の中核機能をエンドツーエンドで使い、シンプルで現実的な UI の中でどのように連携するかを確認します。
前提条件: React、基礎的な JavaScript/TypeScript
作るもの
- 商品グリッドとシンプルなインメモリカートを備えた単一ページの「ショップ」
- 言語切り替えと共有ナビゲーションラベル
- 正しくローカライズされた数値、通貨、複数形
- 任意: 本番ビルド向けのローカル翻訳ストレージ
このチュートリアルで使用するリンク
- Provider: <GTProvider>
- Components: <T>,<Var>,<Num>,<Currency>,<DateTime>,<Branch>,<Plural>,<LocaleSelector>
- Strings and Shared Strings: useGT,msg,useMessages
- Guides: Variables, Branching, Strings, Local Translation Storage, Changing Languages
アプリのセットアップとラップ
パッケージをインストールし、プロバイダでアプリをラップします。
npm i gt-react
npm i --save-dev gtx-cliyarn add gt-react
yarn add --dev gtx-clibun add gt-react
bun add --dev gtx-clipnpm add gt-react
pnpm add --save-dev gtx-cliオプション: スタータープロジェクト(Vite)
ゼロから始める場合は、Vite の React + TypeScript アプリをひな型作成してから GT のパッケージをインストールします:
npm create vite@latest mini-shop -- --template react-ts
cd mini-shop
npm i gt-react
npm i --save-dev gtx-cliその後、下記セクションのファイルを追加します(例: src/main.tsx、src/App.tsx、src/components/*、src/data.ts、src/nav.ts)。
最小構成のプロバイダを用意します。
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { GTProvider } from 'gt-react'; // 参照: /docs/react/api/components/gtprovider
import App from './App';
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <GTProvider locales={["es", "fr"]}> {/* スペイン語とフランス語を有効にする */}
      <App />
    </GTProvider>
  </StrictMode>
);必要に応じて、今のうちに gt.config.json を追加しておきましょう(後で CI やローカル保存に役立ちます):
{
  "defaultLocale": "en",
  "locales": ["es", "fr"]
}開発用 API Keys
このチュートリアルはキーがなくても実行できます(デフォルト言語で表示されます)。開発環境でライブ翻訳を確認し、言語切り替えをテストするには、開発用のキーを追加してください。
詳しくは 本番環境と開発環境 をご覧ください。
VITE_GT_API_KEY="your-dev-api-key"
VITE_GT_PROJECT_ID="your-project-id"REACT_APP_GT_API_KEY="your-dev-api-key"
REACT_APP_GT_PROJECT_ID="your-project-id"- ダッシュボード: https://dash.generaltranslation.com/signup
- または CLI から:
npx gtx-cli auth
シードデータとアプリ構成
小さな商品配列をハードコードし、すべてをクライアントサイドで行います。サーバーもルーティングも使いません。
export type Product = {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: 'USD' | 'EUR';
  inStock: boolean;
  addedAt: string; // ISO date string
};
export const products: Product[] = [
  {
    id: 'p-1',
    name: 'ワイヤレスヘッドホン',
    description: 'ノイズキャンセリング機能付きオーバーイヤー型、30時間バッテリー',
    price: 199.99,
    currency: 'USD',
    inStock: true,
    addedAt: new Date().toISOString()
  },
  {
    id: 'p-2',
    name: 'トラベルマグ',
    description: '二重壁断熱ステンレス製(12oz)',
    price: 24.5,
    currency: 'USD',
    inStock: false,
    addedAt: new Date().toISOString()
  }
];共有ナビゲーションラベルを msg と useMessages で扱う
設定内の共通文字列には msg で印を付け、レンダリング時に useMessages でデコードします。
import { msg } from 'gt-react'; // 参照: /docs/react/api/strings/msg
export const nav = [
  { label: msg('ホーム'), href: '#' },
  { label: msg('商品'), href: '#products' },
  { label: msg('カート'), href: '#cart' }
];import { LocaleSelector, T } from 'gt-react';
import { useMessages } from 'gt-react'; // 参照: /docs/react/api/strings/useMessages
import { nav } from '../nav';
export default function Header() {
  const m = useMessages();
  return (
    <header style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
      <T><h1>ミニショップ</h1></T> {/* 参照: /docs/react/api/components/t */}
      <nav style={{ display: 'flex', gap: 12 }}>
        {nav.map(item => (
          <a key={item.href} href={item.href} title={m(item.label)}>
            {m(item.label)}
          </a>
        ))}
      </nav>
      <div style={{ marginLeft: 'auto' }}>
        <LocaleSelector /> {/* 参照: /docs/react/api/components/localeSelector */}
      </div>
    </header>
  );
}<T>、Variable、Branch、Currency を使った商品カード
JSX の翻訳には <T> を使用します。動的コンテンツは、<Var>、<Num>、<Currency>、<DateTime> などの変数コンポーネントで囲みます。在庫状態は <Branch> で制御します。
import { T, Var, Num, Currency, DateTime, Branch } from 'gt-react';
import type { Product } from '../data';
export default function ProductCard({ product, onAdd }: { product: Product; onAdd: () => void; }) {
  return (
    <div style={{ border: '1px solid #ddd', padding: 12, borderRadius: 8 }}>
      <T>
        <h3><Var>{product.name}</Var></h3>
        <p><Var>{product.description}</Var></p>
        <p>
          価格: <Currency currency={product.currency}>{product.price}</Currency>
        </p>
        <p>
          追加日: <DateTime options={{ dateStyle: 'medium', timeStyle: 'short' }}>{product.addedAt}</DateTime>
        </p>
        <Branch
          branch={product.inStock}
          true={<p>在庫あり</p>}
          false={<p style={{ color: 'tomato' }}>在庫切れ</p>}
        />
        <button onClick={onAdd} disabled={!product.inStock}>
          カートに追加
        </button>
      </T>
    </div>
  );
}複数形ルールと合計を扱うカート
カート内の「X items in cart」を表すには <Plural> を、合計には <Currency> を使用します。<T>、<Var>、<Num> と組み合わせてください。
import { T, Plural, Var, Num, Currency } from 'gt-react';
import type { Product } from '../data';
export default function Cart({ items, onClear }: { items: Product[]; onClear: () => void; }) {
  const total = items.reduce((sum, p) => sum + p.price, 0);
  const itemCount = items.length;
  return (
    <div style={{ borderTop: '1px solid #eee', paddingTop: 12 }}>
      <T>
        <h2>カート</h2>
        <Plural
          n={itemCount}
          zero={<p>カートは空です</p>}
          one={<p>商品が<Num>{itemCount}</Num>点あります</p>}
          other={<p>商品が<Num>{itemCount}</Num>点あります</p>}
        />
        {items.map((p) => (
          <p key={p.id}>
            <Var>{p.name}</Var> — <Currency currency={p.currency}>{p.price}</Currency>
          </p>
        ))}
        <p>
          合計: <Currency currency={items[0]?.currency || 'USD'}>{total}</Currency>
        </p>
        <button onClick={onClear} disabled={itemCount === 0}>カートを空にする</button>
      </T>
    </div>
  );
}useGT を使った属性とプレースホルダー
入力プレースホルダーや ARIA ラベルなどのプレーンな文字列の翻訳には、useGT を使用します。
import { useGT } from 'gt-react';
export default function Search({ onQuery }: { onQuery: (q: string) => void; }) {
  const t = useGT();
  return (
    <input
      type="search"
      placeholder={t('商品を検索...')}
      aria-label={t('検索')}
      onChange={(e) => onQuery(e.target.value)}
      style={{ padding: 8, width: '100%', maxWidth: 320 }}
    />
  );
}すべてを組み合わせる
インメモリのカートとシンプルな検索フィルターを備えたシングルページアプリ。
import { useMemo, useState } from 'react';
import Header from './components/Header';
import Search from './components/Search';
import ProductCard from './components/ProductCard';
import Cart from './components/Cart';
import { products } from './data';
export default function App() {
  const [query, setQuery] = useState('');
  const [cart, setCart] = useState<string[]>([]);
  const filtered = useMemo(() => {
    const q = query.toLowerCase();
    return products.filter(p =>
      p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)
    );
  }, [query]);
  const items = products.filter(p => cart.includes(p.id));
  return (
    <div style={{ margin: '24px auto', maxWidth: 960, padding: 16 }}>
      <Header />
      <div style={{ margin: '16px 0' }}>
        <Search onQuery={setQuery} />
      </div>
      <section id="products" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 16 }}>
        {filtered.map(p => (
          <ProductCard
            key={p.id}
            product={p}
            onAdd={() => setCart(c => (p.inStock ? [...new Set([...c, p.id])] : c))}
          />
        ))}
      </section>
      <section id="cart" style={{ marginTop: 24 }}>
        <Cart
          items={items}
          onClear={() => setCart([])}
        />
      </section>
    </div>
  );
}ローカルで実行
package.json に簡単な開発用スクリプトを追加し、アプリを起動します。
{
  "scripts": {
    "dev": "vite"
  }
}実行する:
npm run dev{
  "scripts": {
    "start": "react-scripts start"
  }
}実行する:
npm start学んだこと
- <T>を使った JSX の翻訳と、- <Var>、- <Num>、- <Currency>、- <DateTime>による動的コンテンツの処理
- <Branch>による条件付きコンテンツの表現と、- <Plural>による数量表現
- useGTを使った属性の翻訳
- msgによるナビゲーション/設定文字列の共有と、- useMessagesを使ったデコード
- <LocaleSelector>を使った言語切り替え
次のステップ
このガイドはどうでしたか?

