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

APP JavaScript
スポンサーリンク

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

4日目のテーマは
「翻訳アプリを“毎日使えるツール”に近づけるために、状態管理・お気に入り保存・UI 改善を行う」
ことです。

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

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

ただし今日は、これらを「アプリとしての完成度」に結びつけます。

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

  • お気に入り翻訳の保存(localStorage)
  • 翻訳履歴の保存(localStorage)
  • 状態管理(state)を導入してコードを整理
  • ローディング表示の強化
  • エラーハンドリングの“パターン化”
  • UI の改善(お気に入りの再翻訳など)

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


状態管理(state)を導入してアプリを整理する

なぜ state が必要なのか

昨日までのコードは、
変数がバラバラに存在していました。

  • 履歴
  • お気に入り
  • ローディング状態
  • 入力テキスト
  • 言語設定

これらをひとつのオブジェクトにまとめることで、
「アプリの現在地」が一目でわかるようになります。

state の基本形

const state = {
  isLoading: false,
  history: [],
  favorites: []
};
JavaScript

状態を更新する関数

function updateState(updates) {
  Object.assign(state, updates);
}
JavaScript

これにより、
updateState({ isLoading: true })
のように、どこからでも状態を更新できます。


お気に入り翻訳を保存する(localStorage)

なぜ保存が必要なのか

翻訳アプリは「何度も使う」ツールです。
お気に入りの翻訳が残っていれば、
ユーザーは毎回入力し直す必要がありません。

保存の基本ルール

localStorage は 文字列しか保存できない ため、
配列やオブジェクトは JSON に変換します。

保存:

localStorage.setItem("lt_favorites", JSON.stringify(state.favorites));
JavaScript

読み込み:

const saved = JSON.parse(localStorage.getItem("lt_favorites") || "[]");
JavaScript

お気に入り追加関数

function addFavorite(original, translated, source, target) {
  state.favorites.unshift({ original, translated, source, target });

  if (state.favorites.length > 10) {
    state.favorites.pop();
  }

  saveFavorites();
  renderFavorites();
}
JavaScript

保存関数

function saveFavorites() {
  localStorage.setItem("lt_favorites", JSON.stringify(state.favorites));
}
JavaScript

読み込み関数

function loadFavorites() {
  const saved = localStorage.getItem("lt_favorites");

  if (!saved) {
    state.favorites = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    state.favorites = Array.isArray(parsed) ? parsed : [];
  } catch (e) {
    console.error("お気に入りの読み込みに失敗しました", e);
    state.favorites = [];
  }

  renderFavorites();
}
JavaScript

お気に入り一覧を UI に表示する

表示関数

function renderFavorites() {
  if (state.favorites.length === 0) {
    favoritesDiv.textContent = "お気に入り翻訳はまだありません。";
    return;
  }

  let html = "<h3>お気に入り翻訳</h3>";

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

  favoritesDiv.innerHTML = html;

  const items = favoritesDiv.querySelectorAll(".fav-item");
  items.forEach((item) => {
    item.addEventListener("click", () => {
      const index = item.dataset.index;
      const fav = state.favorites[index];

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

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

深掘りポイント

お気に入りをクリックすると、
入力欄に値を戻して再翻訳できる
という動きが自然に実現できます。

これは、
翻訳処理が translateText() に一本化されているからです。


翻訳履歴も保存する(localStorage)

履歴の保存

function saveHistory() {
  localStorage.setItem("lt_history", JSON.stringify(state.history));
}
JavaScript

履歴の読み込み

function loadHistory() {
  const saved = localStorage.getItem("lt_history");

  if (!saved) {
    state.history = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    state.history = Array.isArray(parsed) ? parsed : [];
  } catch (e) {
    console.error("履歴の読み込みに失敗しました", e);
    state.history = [];
  }

  renderHistory();
}
JavaScript

履歴追加

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

  if (state.history.length > 10) {
    state.history.pop();
  }

  saveHistory();
  renderHistory();
}
JavaScript

ローディング表示を強化する

状態を見て UI を変える

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

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

深掘りポイント

ローディング表示を関数にまとめることで、

  • ボタン無効化
  • ステータス表示
  • 状態更新

1 行で済む ようになります。


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

翻訳専用の共通関数を作る

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 のすべてのエラーを「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;
  }

  if (source === target) {
    statusDiv.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

4日目のまとめ

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

  • state にアプリの状態を集約
  • お気に入り翻訳を localStorage に保存
  • 翻訳履歴も localStorage に保存
  • お気に入り・履歴から再翻訳できるように
  • ローディング表示を関数化して UI を統一
  • エラーハンドリングをパターン化
  • 翻訳処理を requestTranslate に共通化

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


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

4日目の本質は、

「状態管理と保存を整えると、アプリは一気に“毎日使えるツール”になる」
ということです。

翻訳
履歴
お気に入り
ローディング
エラー表示

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

5日目では、
UI の改善、翻訳の精度向上、入力補助の強化などを行い、
さらに実用的な翻訳アプリに育てていきます。

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