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();
}
}
JavaScript4日目のまとめ
今日やったことを整理すると、こうなります。
- state にアプリの状態を集約
- お気に入り翻訳を localStorage に保存
- 翻訳履歴も localStorage に保存
- お気に入り・履歴から再翻訳できるように
- ローディング表示を関数化して UI を統一
- エラーハンドリングをパターン化
- 翻訳処理を requestTranslate に共通化
どれも新しい文法ではなく、
「fetch / async-await / エラーハンドリングの型をどう整理して使うか」
という話です。
今日いちばん深く理解してほしいこと
4日目の本質は、
「状態管理と保存を整えると、アプリは一気に“毎日使えるツール”になる」
ということです。
翻訳
履歴
お気に入り
ローディング
エラー表示
これらはすべて、
fetch / async-await / try-catch の型の上に乗っています。
5日目では、
UI の改善、翻訳の精度向上、入力補助の強化などを行い、
さらに実用的な翻訳アプリに育てていきます。

