Currency Converter

セットアップ

チュートリアルプロジェクトを設定する

はじめに

これは、gt-next を使って非常にシンプルな Next.js プロジェクトをセットアップする方法を、より詳しく解説するチュートリアルガイドです。 プロジェクトの準備から翻訳まで、最初から最後まで順を追って説明します。 このチュートリアルでは、基本から始めて段階的により高度な概念へと進みます。 このチュートリアルは、TypeScript、Next.js、React の基礎知識があることを前提としています。

本チュートリアルで扱う内容は次のとおりです:

  • 新しい Next.js プロジェクトのセットアップ
  • アプリを翻訳するための <T> コンポーネントの使用
  • 動的コンテンツを翻訳するための <Var><Currency><DateTime><Num> といった変数コンポーネントの使用
  • 条件付きコンテンツを翻訳するための <Plural><Branch> といった分岐コンポーネントの使用
  • アプリでの i18n ルーティングの使用

作成するアプリは、通貨間の換算レートを確認できるシンプルなアプリです。 可能な限り最小構成にするため、インラインスタイルと gt-next ライブラリのみを使用します。 この例は、GeeksforGeeks の Currency Converter チュートリアルを基に作成しています。

Next.js アプリをセットアップする

まず、新しい Next.js アプリを作成しましょう。次のコマンドを実行してください。

npx create-next-app@latest

これでセットアップウィザードが起動し、アプリの名称と使用するテンプレートを選択できます。 このチュートリアルでは、名称は currencies とし、TypeScript を使用するか尋ねられたら Yes を選択してください。

プロジェクトディレクトリに移動して、アプリを起動しましょう!

cd currencies
npm install
npm run dev

これでアプリが http://localhost:3000 で起動します。

コンテンツを追加しましょう!

アプリのセットアップが完了したので、シンプルな通貨コンバーターを表示するように、アプリのコンテンツを上書きしましょう。 以下のコードをそのまま src/app/page.tsxsrc/app/layout.tsx にコピー&ペーストしてください。

今は仕組みについて深く気にしなくて大丈夫です。 このコードは通貨為替APIへの取得処理を擬似的に行い、2つの通貨間の為替レートを表示するだけです。

src/app/page.tsx
"use client";

import { useEffect, useState, useCallback } from "react";

// 2つの通貨間の為替レートのマップ(from -> to)
type ExchTable = Record<string, Record<string, number>>;

const EXCH_RATES: ExchTable = {
  usd: { usd: 1, inr: 73.5, eur: 0.85, jpy: 105.45, gbp: 0.72 },
  inr: { usd: 0.014, inr: 1, eur: 0.012, jpy: 1.46, gbp: 0.01 },
  eur: { usd: 1.18, inr: 85.5, eur: 1, jpy: 123.5, gbp: 0.85 },
  jpy: { usd: 0.0095, inr: 0.68, eur: 0.0081, jpy: 1, gbp: 0.0068 },
  gbp: { usd: 1.39, inr: 99.5, eur: 1.17, jpy: 146.5, gbp: 1 },
};

// ボタンのスタイル設定
const buttonStyle = {
  backgroundColor: "#007bff",
  color: "white",
  border: "none",
  padding: "10px 20px",
  cursor: "pointer",
  borderRadius: "5px",
  fontSize: "16px",
  margin: "20px",
};

/**
 * 現在の為替レートを取得するAPIフェッチリクエストをシミュレートする関数です。
 * 為替レートを返す前に1秒間待機します。
 * @returns 2つの通貨間の為替レート
 */
async function fetchExchangeRate(from: string, to: string): Promise<number> {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return EXCH_RATES[from][to];
}

function Page() {
  // 為替レート
  const [info, setInfo] = useState<ExchTable>({});
  const options = ["usd", "inr", "eur", "jpy", "gbp"];

  // 通貨
  const [from, setFrom] = useState("usd");
  const [to, setTo] = useState("inr");

  // 値
  const [input, setInput] = useState(0);
  const [output, setOutput] = useState(0);

  // 通貨を変換する関数
  const convert = useCallback(() => {
    if (info?.[from]?.[to]) {
      const rate = info[from][to];
      setOutput(input * rate);
    } else {
      setOutput(0);
    }
  }, [info, input, to, from]);

  // fromまたはto通貨が変更されるたびにAPIを呼び出す
  useEffect(() => {
    // 為替レートが既に存在する場合は変換を実行
    if (info?.[from]?.[to]) {
      convert();
      return;
    }
    // 為替レートを取得
    (async () => {
      const response = await fetchExchangeRate(from, to);
      // 既存の情報を上書きせずに新しいレスポンスを追加
      setInfo((prevInfo) => ({
        ...prevInfo,
        [from]: {
          ...(prevInfo?.[from] || undefined),
          [to]: response,
        },
      }));
    })();
  }, [from, to, convert, info]);

  // ユーザーが通貨を切り替えるたびにconvertを呼び出す
  useEffect(() => {
    convert();
  }, [info, convert]);

  // 2つの通貨を入れ替える関数
  function flip() {
    const temp = from;
    setFrom(to);
    setTo(temp);
  }

  return (
    <div style={{ margin: "0 auto", width: "50%", textAlign: "center" }}>
      <div style={{ margin: "20px 0", paddingBottom: "20px" }}>
        <h1 style={{ fontSize: "2.5em", fontWeight: "bold" }}>
          通貨コンバーター
        </h1>
      </div>
      <div style={{ flex: 2, textAlign: "center", margin: "20px 0" }}>
        <h3>金額</h3>
        <input
          type="text"
          placeholder="金額を入力してください"
          style={{ textAlign: "center" }}
          onChange={(e) => {
            setInput(Number(e.target.value));
          }}
        />
      </div>
      <div
        style={{ display: "flex", justifyContent: "center", margin: "20px 0" }}
      >
        <div style={{ flex: 1, textAlign: "center" }}>
          <label htmlFor="from">
            <h3>変換元</h3>
          </label>
          <select
            name="from"
            id="from"
            value={from}
            onChange={(e) => setFrom(e.target.value)}
          >
            {options.map((option) => (
              <option key={option} value={option}>
                {option.toUpperCase()}
              </option>
            ))}
          </select>
        </div>
        <div style={{ flex: 1, textAlign: "center" }}>
          <button
            onClick={() => {
              flip();
            }}
            style={buttonStyle}
          >
            入れ替え
          </button>
        </div>
        <div style={{ flex: 1, textAlign: "center" }}>
          <label htmlFor="to">
            <h3>変換先</h3>
          </label>
          <select
            name="to"
            id="to"
            value={to}
            onChange={(e) => setTo(e.target.value)}
          >
            {options.map((option) => (
              <option key={option} value={option}>
                {option.toUpperCase()}
              </option>
            ))}
          </select>
        </div>
      </div>
      <div style={{ margin: "0 auto", width: "50%", textAlign: "center" }}>
        <button
          onClick={() => {
            convert();
          }}
          style={buttonStyle}
        >
          変換
        </button>
        <h2>変換後の金額:</h2>
        <p>{input + " " + from + " = " + output.toFixed(2) + " " + to}</p>
      </div>
    </div>
  );
}

export default Page;
src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "通貨換算ツール",
  description: "シンプルな通貨換算ツール",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

まとめ

このガイドはどうでしたか?

セットアップ