1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[プロジェクト] 韓国語 ↔ 日本語、多言語サポート追加記 - HabitFlow 開発記 (3編)

1
Posted at

現場とコードの間 - 7ngenious
2025.02.11

🌏 「日本で働いているのに、アプリは韓国語だけ?」

デプロイ成功後、ふと思った。

「日本に住みながら日本人の同僚に見せたいのに... 全部韓国語だ」

日本の自動車製造現場で働きながら、毎日日本語と韓国語を行き来しながら生活している。JLPT N1を取得し、日本語で業務をするが、個人プロジェクトはいつも韓国語だけで作っていた。

今回は違わなければならないと思った。


🎯 目標:ボタン一つで言語切り替え

要件

機能:

  • 🇰🇷 韓国語 ↔ 🇯🇵 日本語切り替え
  • ボタンクリックで即座に切り替え
  • 選択した言語を保存(LocalStorage)
  • すべてのUIテキスト翻訳
  • 日付フォーマットも言語別に変更

UI:

  • ヘッダー右側に国旗ボタン
  • 円形デザイン
  • クリック一つで切り替え

📐 設計:React Contextでシンプルに

なぜContext API?

他の選択肢:

  • Redux:重すぎる
  • i18next:設定が複雑
  • Props drilling:すべてのコンポーネントに渡す?非効率的

Context APIがぴったりだ!

  • React内蔵機能
  • 軽くてシンプル
  • グローバル状態管理可能

💻 実装:3段階戦略

Step 1: 翻訳ファイル作成(30分)

構造:

src/
└── locales/
    ├── ko.js  # 韓国語
    └── ja.js  # 日本語

ko.js:

export const ko = {
  header: {
    title: "HabitFlow",
    subtitle: "습관을 흐름으로 만들어보세요"
  },
  habitList: {
    title: "내 습관",
    addButton: "습관 추가",
    emptyState: {
      line1: "아직 습관이 없습니다.",
      line2: "위의 '습관 추가' 버튼을 눌러 시작하세요!"
    }
  },
  // ... 合計50個以上のテキスト
};

ja.js:

export const ja = {
  header: {
    title: "HabitFlow",
    subtitle: "習慣を流れにしましょう"
  },
  habitList: {
    title: "マイ習慣",
    addButton: "習慣を追加",
    emptyState: {
      line1: "まだ習慣がありません。",
      line2: "上の「習慣を追加」ボタンを押して始めましょう!"
    }
  },
  // ...
};

翻訳作業:

  • 合計50個以上のテキスト
  • 日本語はJLPT N1知識活用
  • 自然な日本語表現を心がける
  • 例:「습관 추가」→「習慣を追加」(助詞「を」使用)

Step 2: Context生成(20分)

// src/contexts/Languagecontext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import { ko } from '../locales/ko';
import { ja } from '../locales/ja';

const LanguageContext = createContext();

const translations = { ko, ja };

export function LanguageProvider({ children }) {
  // LocalStorageから保存された言語を読み込み
  const [language, setLanguage] = useState(() => {
    const saved = localStorage.getItem('language');
    return saved || 'ko'; // デフォルト値:韓国語
  });

  // 言語変更時に保存
  useEffect(() => {
    localStorage.setItem('language', language);
  }, [language]);

  const toggleLanguage = () => {
    setLanguage(prev => prev === 'ko' ? 'ja' : 'ko');
  };

  const t = translations[language];

  return (
    <LanguageContext.Provider value={{ language, toggleLanguage, t }}>
      {children}
    </LanguageContext.Provider>
  );
}

// Custom Hook
export function useLanguage() {
  const context = useContext(LanguageContext);
  if (!context) {
    throw new Error('useLanguage must be used within LanguageProvider');
  }
  return context;
}

核心ポイント:

  • toggleLanguage:'ko' ↔ 'ja' 切り替え
  • t:現在言語の翻訳オブジェクト
  • LocalStorage:ブラウザ更新しても言語維持

Step 3: 言語切り替えボタン(15分)

// src/components/Languageswitcher.jsx
import { useLanguage } from '../contexts/Languagecontext';

export default function LanguageSwitcher() {
  const { language, toggleLanguage } = useLanguage();

  return (
    <button
      className="language-switcher-flag"
      onClick={toggleLanguage}
      title={language === 'ko' ? '日本語に切り替え' : '한국어로 전환'}
    >
      <span className="flag-emoji">
        {language === 'ko' ? '🇯🇵' : '🇰🇷'}
      </span>
    </button>
  );
}

デザイン意図:

  • 国旗のみ表示(テキストなし)
  • 円形ボタン
  • hover時に拡大 + 回転アニメーション

Step 4: すべてのコンポーネントに適用(1時間)

App.jsx:

import { LanguageProvider, useLanguage } from './contexts/Languagecontext';

function AppContent() {
  const { t } = useLanguage();
  
  return (
    <div className="app">
      <header>
        <h1>{t.header.title}</h1>
        <p>{t.header.subtitle}</p>
        <LanguageSwitcher />
      </header>
      {/* ... */}
    </div>
  );
}

function App() {
  return (
    <LanguageProvider>
      <AppContent />
    </LanguageProvider>
  );
}

各コンポーネント:

// HabitList.jsx
import { useLanguage } from '../contexts/Languagecontext';

export default function HabitList({ habits, onAddHabit, onDeleteHabit }) {
  const { t } = useLanguage();
  
  return (
    <div>
      <h2>{t.habitList.title}</h2>
      <button>{t.habitList.addButton}</button>
      {/* ... */}
    </div>
  );
}

適用したコンポーネント:

  • HabitList:フォームラベル、ボタン、メッセージ
  • DailyCheck:日付フォーマット、達成率テキスト
  • MonthCalendar:曜日、週次、凡例
  • ProgressChart:チャートタイトル、軸ラベル、統計

🎨 日付フォーマットも言語別に!

date-fnsのlocale活用:

import { format } from 'date-fns';
import { ko as koLocale, ja as jaLocale } from 'date-fns/locale';

const { language } = useLanguage();
const dateLocale = language === 'ko' ? koLocale : jaLocale;
const dateFormat = language === 'ko' ? 'yyyy년 M월 d일 EEEE' : 'yyyy年M月d日 EEEE';

const formattedDate = format(new Date(), dateFormat, { locale: dateLocale });

結果:

  • 韓国語:「2025년 2월 11일 화요일」
  • 日本語:「2025年2月11日 火曜日」

曜日も変更:

  • 韓国語:일、월、화、수、목、금、토
  • 日本語:日、月、火、水、木、金、土

😱 問題発生:ファイル名大文字小文字地獄

問題 1: 白い画面

デプロイ後にアクセスすると白い画面

原因:

# ko.jsファイルが空だった!
-rw-r--r--  0  2 11 21:30 ko.js  # 0バイト!

# jp.jsがあるがja.jsであるべき
-rw-r--r--  1864  2 11 22:04 jp.js

解決:

  • ko.js内容を埋める
  • jp.js → ja.jsに名前変更

問題 2: ビルドエラー

Could not resolve "./contexts/Languagecontext" from "src/App.jsx"

原因:

  • macOS:Languagecontext.jsx = LanguageContext.jsx(同じファイル)
  • Linux(Vercel):違うファイルとして認識!
  • GitにはLanguageContext.jsxとして保存された
  • ローカルはLanguagecontext.jsx

解決:

# 1. Gitキャッシュ整理
git rm --cached src/contexts/LanguageContext.jsx

# 2. 現在のファイル追加
git add src/contexts/Languagecontext.jsx

# 3. すべてのimportを小文字に統一
# './contexts/LanguageContext' → './contexts/Languagecontext'

学んだこと:

  • 大文字小文字の一貫性重要!
  • macOSでは問題なくてもLinuxでエラー
  • Gitは大文字小文字を区別する

問題 3: チェックボックスUIバグ

チェックボックスが横に長く伸びる

原因:

.checkbox {
  width: 28px;
  /* min-widthがなくて伸びる! */
}

解決:

.habit-checkbox-custom {
  width: 28px;
  height: 28px;
  min-width: 28px;  /* 固定! */
  min-height: 28px;
}

改善:

  • 実際の<input type="checkbox">使用
  • カスタムデザイン + SVGチェックマーク
  • チェック時にアニメーション追加

問題 4: カレンダー点がクリックできない

原因:

// divはクリックイベントが正しく伝わらない
<div onClick={() => onToggleCheck(habit.id, dateStr)} />

解決:

// buttonに変更 + イベントバブリング防止
<button
  onClick={(e) => {
    e.stopPropagation();
    onToggleCheck(habit.id, dateStr);
  }}
/>

🎨 UI改善作業

1. 言語ボタン:国旗でいっぱい

Before:

[🇰🇷 KO]  ← 長方形、テキストあり

After:

🇰🇷  ← 円形ボタン、国旗のみ
.language-switcher-flag {
  width: 44px;
  height: 44px;
  border-radius: 50%;  /* 完全な円 */
  font-size: 2rem;     /* 国旗大きく */
}

2. チェックボックス:見えるデザイン

<label className="habit-check-item">
  <input type="checkbox" className="habit-checkbox-input" />
  <div className="habit-checkbox-custom">
    {isChecked && (
      <svg className="checkmark-icon">
        <polyline points="20 6 9 17 4 12" />
      </svg>
    )}
  </div>
</label>

CSS:

/* 実際のチェックボックスを隠す */
.habit-checkbox-input {
  position: absolute;
  opacity: 0;
}

/* カスタムチェックボックス */
.habit-checkbox-custom {
  width: 28px;
  height: 28px;
  border: 2.5px solid;
  border-radius: 8px;
}

/* チェック時のアニメーション */
@keyframes checkPop {
  0% { transform: scale(0); }
  50% { transform: scale(1.2); }
  100% { transform: scale(1); }
}

3. カレンダー点:クリック可能

<button
  className={`habit-dot ${isChecked ? 'checked' : 'unchecked'}`}
  style={{ 
    backgroundColor: isChecked ? habit.color : '#e5e7eb',
    borderColor: habit.color
  }}
/>

CSS:

.habit-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  cursor: pointer;
}

.habit-dot:hover {
  transform: scale(1.4);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

.habit-dot.unchecked {
  border: 2px solid;
  background: white !important;
}

📊 最終結果

翻訳統計

  • 翻訳されたテキスト:50個以上
  • コンポーネント:4個すべて適用
  • 日付フォーマット:2つの言語
  • 曜日:2つの言語

開発時間

翻訳ファイル作成:30分
Context生成:20分
言語ボタン:15分
コンポーネント適用:1時間
UI改善:1時間
バグ修正:2時間
─────────────────
総開発時間:約5時間

🎓 学んだこと

React Context API

  • グローバル状態管理方法
  • Custom Hookパターン
  • Providerパターン

国際化(i18n)

  • 翻訳ファイル構造化
  • date-fns locale活用
  • 動的テキスト置換

ファイルシステム

  • macOS vs Linux大文字小文字の違い
  • Git大文字小文字処理
  • import経路一貫性

UI/UX

  • アクセシビリティ(button vs div)
  • イベントバブリング
  • SVG活用
  • CSSアニメーション

💭 所感

「多言語サポート、考えよりも複雑だった」

最初は「ただテキストを変えればいいでしょう?」と思った。

しかし:

  • 50個以上のテキスト翻訳
  • 日付フォーマット処理
  • ファイル名大文字小文字イシュー
  • UIバグ修正
  • 5時間の開発 + デバッグ

それでもやりがいがあった。

今は日本人の同僚にも自信を持って見せられる。
「日本語でも使えるよ!」

そして何より:

  • Context API完璧理解
  • 国際化経験蓄積
  • 実戦デバッグスキル向上

🚀 次のステップ

Phase 1: より多くの言語

  • 英語追加
  • 自動言語検出(ブラウザ設定)

Phase 2: バックエンド連動

  • Spring Boot APIサーバー
  • MySQLデータベース
  • JWT認証

📸 完成画面

**韓国語画面:
스크린샷 2026-02-11 오후 10.58.28.png

日本語画面:
**스크린샷 2026-02-11 오후 10.58.41.png

🔗 プロジェクトリンク

今🇰🇷と🇯🇵ボタンをクリックしてみてください!


🙏 終わりに

PLCエンジニアからウェブ開発者への転換、そして多言語サポートまで。

韓国と日本を行き来しながら生きてきた経験がプロジェクトに込められた。

次のプロジェクトではSpring Bootでバックエンドまで! 💪


シリーズ完結

  • 0編:習慣を「感じられる」ように
  • 1編:バイブコーディングで1日でMVP完成
  • 2編:デプロイ後Git Pushができない時 - HTTP 400エラー解決記
  • 3編:韓国語 ↔ 日本語、多言語サポート追加記(現在)

Tags: #React #多言語 #i18n #ContextAPI #日本語 #JLPTN1 #国際化 #デバッグ


Made with ❤️ by 7ngenious

韓国語と日本語で習慣を管理しましょう

🌟 Star
🐛 Report Bug
🚀 Try It

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?