JavaScript | 1 日 120 分 × 7 日アプリ学習:API通信アプリ(LibreTranslate API)

APP JavaScript
スポンサーリンク

3日目のゴールと今日やること

3日目のテーマは
「翻訳アプリを“使いやすく・賢く”するために、入力補助・自動翻訳・履歴の強化を行う」
ことです。

技術的な柱はいつも通りこの 3 つです。

  • fetch
  • Promise / async-await
  • エラーハンドリング

ただし今日は、これらを「アプリとしての使い勝手」に結びつけます。

具体的には次の機能を追加します。

  • 入力中に自動翻訳(一定時間入力が止まったら翻訳)
  • 翻訳履歴の強化(クリックで再翻訳)
  • ローディング表示の改善(自動翻訳時も対応)
  • エラーハンドリングの“パターン化”
  • fetch の共通化(翻訳専用関数を作る)

これらはすべて fetch / async-await の型をそのまま使い回す ことで実現できます。


自動翻訳(入力が止まったら翻訳)を実装する

なぜ自動翻訳が必要なのか

翻訳アプリを使うとき、
「毎回ボタンを押すのが面倒」と感じることがあります。

そこで、
入力が 500ms 止まったら自動で翻訳する
という仕組みを作ります。

これを「デバウンス」と呼びます。

デバウンスの基本イメージ

let timerId = null;

inputText.addEventListener("input", () => {
  clearTimeout(timerId);
  timerId = setTimeout(() => {
    translateText();
  }, 500);
});
JavaScript

ポイントは、

  • 入力のたびにタイマーをリセット
  • 最後の入力から 500ms 経ったら翻訳

という流れです。

自動翻訳にローディング表示を対応させる

自動翻訳でも、
ローディング中はボタンを無効化し、状態を表示する
というルールは変わりません。

function startLoading(message) {
  statusDiv.textContent = message || "翻訳中です…";
  translateButton.disabled = true;
}

function endLoading() {
  translateButton.disabled = false;
}
JavaScript

自動翻訳でもこの関数を使い回します。


翻訳処理を「共通関数」にまとめる

なぜ共通化が必要なのか

3日目では、

  • ボタンを押したときの翻訳
  • 自動翻訳
  • 履歴からの再翻訳

など、翻訳処理を呼ぶ場面が増えます。

そこで、
翻訳処理をひとつの関数にまとめる
ことで、コードの重複を防ぎます。

共通の翻訳関数(重要)

async function requestTranslate(text, source, target) {
  try {
    const response = await fetch("https://libretranslate.com/translate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        q: text,
        source: source,
        target: target,
        format: "text"
      })
    });

    if (!response.ok) {
      throw new Error(`HTTPエラー(${response.status})`);
    }

    const data = await response.json();

    if (!data.translatedText) {
      throw new Error("翻訳結果が取得できませんでした。");
    }

    return data.translatedText;

  } catch (error) {
    throw new Error(error.message);
  }
}
JavaScript

ここでの深掘りポイントは、

  • fetch のエラー
  • HTTP エラー
  • JSON パースエラー
  • API の中身が想定外

をすべて throw new Error(…) に統一していることです。

これにより、
上位の関数は「翻訳に失敗した」という事実だけを扱えばよくなります。


翻訳のメイン関数を“読みやすい流れ”にする

translateText の完成形

async function translateText() {
  const text = inputText.value.trim();
  const source = sourceLang.value;
  const target = targetLang.value;

  if (!text) {
    statusDiv.textContent = "翻訳する文章を入力してください。";
    resultDiv.textContent = "";
    return;
  }

  startLoading("翻訳中です…");
  resultDiv.textContent = "";

  try {
    const translated = await requestTranslate(text, source, target);

    statusDiv.textContent = "翻訳に成功しました。";
    resultDiv.textContent = translated;

    addHistory(text, translated, source, target);

  } catch (error) {
    statusDiv.textContent = `翻訳中にエラーが発生しました:${error.message}`;
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

この関数は、上から読むと“物語”になっています。

  • 入力チェック
  • ローディング開始
  • 共通関数で翻訳
  • 成功したら結果表示+履歴追加
  • 失敗したらエラーメッセージ
  • 最後にローディング終了

fetch の細かい処理はすべて requestTranslate に隠れているため、
とても読みやすくなっています。


翻訳履歴を「再翻訳できる UI」にする

履歴の構造

const history = [];
JavaScript

履歴追加

function addHistory(original, translated, source, target) {
  history.unshift({ original, translated, source, target });

  if (history.length > 5) {
    history.pop();
  }

  renderHistory();
}
JavaScript

履歴表示(クリックで再翻訳)

function renderHistory() {
  if (history.length === 0) {
    historyDiv.textContent = "翻訳履歴はまだありません。";
    return;
  }

  let html = "<h3>翻訳履歴</h3>";

  history.forEach((item, index) => {
    html += `
      <div class="history-item" data-index="${index}">
        <p><strong>${item.original}</strong></p>
        <p>→ ${item.translated}</p>
        <p>(${item.source}${item.target})</p>
      </div>
    `;
  });

  historyDiv.innerHTML = html;

  const items = historyDiv.querySelectorAll(".history-item");
  items.forEach((item) => {
    item.addEventListener("click", () => {
      const index = item.dataset.index;
      const h = history[index];

      inputText.value = h.original;
      sourceLang.value = h.source;
      targetLang.value = h.target;

      translateText();
    });
  });
}
JavaScript

ここでの深掘りポイントは、

  • 履歴をクリックすると、入力欄に値を戻して再翻訳
  • 翻訳処理は translateText に一本化しているため、再利用が簡単

という「責務分離の効果」です。


エラーハンドリングを“パターン化”する

翻訳エラーの種類を整理する

翻訳アプリでは、次のようなエラーが起きます。

  • ネットワークエラー
  • HTTP エラー
  • JSON パースエラー
  • API の中身が想定外
  • 入力が空
  • 言語が同じ(en → en など)

これらをすべて「ユーザーに伝わる形」に変換します。

例:言語が同じときのチェック

if (source === target) {
  statusDiv.textContent = "翻訳元と翻訳先が同じです。別の言語を選んでください。";
  return;
}
JavaScript

例:ネットワークエラーの表示

catch (error) {
  statusDiv.textContent = `翻訳中にエラーが発生しました:${error.message}`;
}
JavaScript

ここでのポイントは、

  • エラーの種類ごとに「ユーザーが理解できる言葉」に変換する
  • 技術的なエラー文をそのまま出さない

ということです。


ローディング表示を“自動翻訳にも対応”させる

自動翻訳でも translateText を呼ぶだけ

自動翻訳はこう書きます。

let autoTimer = null;

inputText.addEventListener("input", () => {
  clearTimeout(autoTimer);

  autoTimer = setTimeout(() => {
    translateText();
  }, 600);
});
JavaScript

translateText の中でローディング処理をしているため、
自動翻訳でも UI が整ったまま動きます。


3日目のまとめ

今日やったことを整理すると、こうなります。

  • 自動翻訳(デバウンス)を実装
  • 翻訳処理を requestTranslate に共通化
  • translateText を“読みやすい流れ”に整理
  • 翻訳履歴をクリックで再翻訳できるように
  • エラーハンドリングをパターン化
  • ローディング表示を自動翻訳にも対応

どれも新しい文法ではなく、
「fetch / async-await / エラーハンドリングの型をどう再利用するか」
という話です。


今日いちばん深く理解してほしいこと

3日目の本質は、

「API 通信の型をひとつ作れば、機能はどんどん増やせる」
ということです。

翻訳
自動翻訳
履歴
再翻訳
エラー表示
ローディング

これらは全部、
同じ fetch / async-await / try-catch の型の上に乗っています。

4日目では、
この翻訳アプリに「お気に入り翻訳」「ローカルストレージ保存」「UI の改善」などを加えて、
さらに実用的なツールに育てていきます。

タイトルとURLをコピーしました