Feature: Add i18n (en/ko/ja), 3-way theme (system/light/dark), and local demo preview mode

This commit is contained in:
Wonhee Han
2026-08-20 14:07:42 +09:00
parent dbfaa0fcb2
commit 3710c313e9
13 changed files with 1611 additions and 655 deletions
+272 -261
View File
@@ -1,288 +1,299 @@
"use client";
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
export type Locale = "en" | "ko" | "ja";
export type Language = "en" | "ko" | "ja";
// ============================================================
// TRANSLATIONS
// ============================================================
const translations = {
export const translations = {
en: {
auth: {
welcome: "Welcome back",
signIn: "Sign in",
signInSub: "Sign in to your account",
createAccount: "Create account",
createAccountSub: "Start organizing your tasks today",
email: "Email",
password: "Password",
passwordHint: "Min. 8 characters",
displayName: "Display Name",
namePlaceholder: "Your name",
noAccount: "Don't have an account?",
hasAccount: "Already have an account?",
createOne: "Create one",
signInLink: "Sign in",
signingIn: "Signing in...",
creating: "Creating account...",
invalidCredentials: "Invalid email or password.",
registrationFailed: "Registration failed.",
},
sidebar: {
lists: "Lists",
newList: "New List",
create: "Create",
cancel: "Cancel",
listName: "List name",
importTasks: "Import Tasks",
signOut: "Sign out",
deleteListConfirm: "Delete this list and all its tasks?",
},
tasks: {
addTask: "Add a task...",
add: "Add",
showDone: "Show done",
hideDone: "Hide done",
noTasks: "No tasks yet. Add one below!",
selectList: "Select a list to get started",
completed: "Completed",
loading: "Loading...",
},
detail: {
priority: "Priority",
dueDate: "Due Date",
notes: "Notes",
notesPlaceholder: "Add notes, details, or anything you need to remember\u2026\n\nMarkdown: **bold**, *italic*, # heading, - list, - [ ] checkbox",
subtasks: "Sub-tasks",
addSubtask: "Add sub-task\u2026",
autoSaved: "Auto-saved",
saving: "Saving\u2026",
delete: "Delete task",
deleteConfirm: "Delete this task?",
close: "Close",
clear: "Clear",
created: "Created",
},
priority: { none: "None", low: "Low", medium: "Medium", high: "High" },
settings: {
theme: "Theme",
language: "Language",
system: "System",
light: "Light",
dark: "Dark",
},
import: {
title: "Import Tasks",
targetList: "Target List",
file: "File (CSV or ICS)",
tip: "TickTick: Settings \u2192 Export \u2192 Export as CSV or iCalendar",
cancel: "Cancel",
import: "Import",
importing: "Importing...",
error: "Error",
},
list: { create: "Create list", color: "Color" },
},
// Auth
appName: "CheckFlow",
tagline: "Your Personal Todo",
welcomeBack: "Welcome back",
signInSubtitle: "Sign in to your account",
createAccount: "Create account",
createAccountSubtitle: "Start organizing your tasks today",
displayName: "Display Name",
email: "Email",
password: "Password",
min8Chars: "Min. 8 characters",
signIn: "Sign in",
signingIn: "Signing in...",
createBtn: "Create account",
creatingBtn: "Creating account...",
alreadyHaveAccount: "Already have an account?",
dontHaveAccount: "Don't have an account?",
tryDemoMode: "Try Demo Mode (No DB Required)",
demoBadge: "Demo Mode",
signOut: "Sign out",
// Sidebar
lists: "Lists",
newList: "New List",
listNamePlaceholder: "List name",
create: "Create",
cancel: "Cancel",
deleteListConfirm: "Delete this list and all its tasks?",
importTasks: "Import Tasks",
importModalTitle: "Import Tasks",
targetList: "Target List",
fileSelectLabel: "File (CSV or ICS from TickTick)",
tickTickExportHint: "TickTick: Settings → Export → Export as CSV or iCalendar",
importBtn: "Import",
importing: "Importing...",
theme: "Theme",
themeSystem: "System",
themeLight: "Light",
themeDark: "Dark",
language: "Language",
// Task List
tasks: "Tasks",
hideDone: "Hide done",
showDone: "Show done",
completedSection: "Completed",
addTaskPlaceholder: "Add a task...",
add: "Add",
noTasksYet: "No tasks yet. Add one below!",
selectListToStart: "Select a list to get started",
loading: "Loading...",
today: "Today",
tomorrow: "Tomorrow",
// Task Detail
taskTitlePlaceholder: "Task title",
priority: "Priority",
priorityNone: "None",
priorityLow: "Low",
priorityMedium: "Medium",
priorityHigh: "High",
dueDate: "Due Date",
clear: "Clear",
notes: "Notes",
notesPlaceholder: "Add notes, details, or anything you need to remember…\n\nMarkdown supported: **bold**, *italic*, # heading, - list, - [ ] checkbox",
subtasks: "Sub-tasks",
addSubtaskPlaceholder: "Add sub-task…",
deleteTaskConfirm: "Delete this task?",
autoSaved: "Auto-saved",
saving: "Saving…",
created: "Created",
// Toolbars
bold: "Bold",
italic: "Italic",
heading: "Heading",
bulletList: "Bullet list",
numberedList: "Numbered list",
checkbox: "Checkbox",
code: "Code",
},
ko: {
auth: {
welcome: "다시 오셨군요",
signIn: "로그인",
signInSub: "계정에 로그인하세요",
createAccount: "계정 만들기",
createAccountSub: "오늘부터 할 일을 정리해보세요",
email: "이메일",
password: "비밀번호",
passwordHint: "최소 8자",
displayName: "이름",
namePlaceholder: "표시될 이름",
noAccount: "계정이 없으신가요?",
hasAccount: "이미 계정이 있으신가요?",
createOne: "만들기",
signInLink: "로그인",
signingIn: "로그인 중...",
creating: "계정 생성 중...",
invalidCredentials: "이메일 또는 비밀번호가 올바르지 않습니다.",
registrationFailed: "회원가입에 실패했습니다.",
},
sidebar: {
lists: "목록",
newList: "새 목록",
create: "만들기",
cancel: "취소",
listName: "목록 이름",
importTasks: "가져오기",
signOut: "로그아웃",
deleteListConfirm: "이 목록과 모든 할 일을 삭제할까요?",
},
tasks: {
addTask: "할 일 추가...",
add: "추가",
showDone: "완료 표시",
hideDone: "완료 숨기기",
noTasks: "할 일이 없습니다. 아래에서 추가하세요!",
selectList: "목록을 선택하여 시작하세요",
completed: "완료됨",
loading: "불러오는 중...",
},
detail: {
priority: "우선순위",
dueDate: "마감일",
notes: "메모",
notesPlaceholder: "메모, 세부 정보, 기억해야 할 내용을 추가하세요\u2026\n\nMarkdown: **굵게**, *기울임*, # 제목, - 목록, - [ ] 체크박스",
subtasks: "하위 할 일",
addSubtask: "하위 항목 추가\u2026",
autoSaved: "자동 저장됨",
saving: "저장 중\u2026",
delete: "삭제",
deleteConfirm: "이 할 일을 삭제할까요?",
close: "닫기",
clear: "초기화",
created: "생성일",
},
priority: { none: "없음", low: "낮음", medium: "보통", high: "높음" },
settings: {
theme: "테마",
language: "언어",
system: "시스템",
light: "라이트",
dark: "다크",
},
import: {
title: "할 일 가져오기",
targetList: "대상 목록",
file: "파일 (CSV 또는 ICS)",
tip: "TickTick: 설정 \u2192 내보내기 \u2192 CSV 또는 iCalendar로 내보내기",
cancel: "취소",
import: "가져오기",
importing: "가져오는 중...",
error: "오류",
},
list: { create: "목록 만들기", color: "색상" },
},
// Auth
appName: "CheckFlow",
tagline: "나만의 똑똑한 할 일 관리",
welcomeBack: "다시 오신 것을 환영합니다",
signInSubtitle: "계정에 로그인하세요",
createAccount: "계정 생성",
createAccountSubtitle: "오늘부터 할 일을 체계적으로 정리해보세요",
displayName: "이름 (닉네임)",
email: "이메일",
password: "비밀번호",
min8Chars: "8자 이상 입력",
signIn: "로그인",
signingIn: "로그인 중...",
createBtn: "계정 만들기",
creatingBtn: "계정 생성 중...",
alreadyHaveAccount: "이미 계정이 있으신가요?",
dontHaveAccount: "계정이 없으신가요?",
tryDemoMode: "체험 모드로 둘러보기 (DB 연결 불필요)",
demoBadge: "체험 모드",
signOut: "로그아웃",
// Sidebar
lists: "목록",
newList: "새 목록",
listNamePlaceholder: "목록 이름",
create: "생성",
cancel: "취소",
deleteListConfirm: "이 목록과 포함된 모든 할 일을 삭제하시겠습니까?",
importTasks: "할 일 가져오기 (Import)",
importModalTitle: "할 일 가져오기",
targetList: "저장할 목록",
fileSelectLabel: "파일 선택 (TickTick CSV 또는 ICS)",
tickTickExportHint: "TickTick: 설정 → 데이터 내보내기 → CSV 또는 iCalendar",
importBtn: "가져오기",
importing: "가져오는 중...",
theme: "테마",
themeSystem: "시스템 설정",
themeLight: "라이트 모드",
themeDark: "다크 모드",
language: "언어",
// Task List
tasks: "할 일",
hideDone: "완료 숨김",
showDone: "완료 보기",
completedSection: "완료됨",
addTaskPlaceholder: "새로운 할 일 추가...",
add: "추가",
noTasksYet: "아직 등록된 할 일이 없습니다. 아래에서 추가해보세요!",
selectListToStart: "목록을 선택해 시작하세요",
loading: "불러오는 중...",
today: "오늘",
tomorrow: "내일",
// Task Detail
taskTitlePlaceholder: "할 일 제목",
priority: "우선순위",
priorityNone: "없음",
priorityLow: "낮음",
priorityMedium: "보통",
priorityHigh: "높음",
dueDate: "마감일",
clear: "지우기",
notes: "메모장",
notesPlaceholder: "상세 내용이나 기억해야 할 메모를 자유롭게 작성하세요…\n\n마크다운 지원: **굵게**, *기울임*, # 제목, - 리스트, - [ ] 체크박스",
subtasks: "하위 할 일 (Sub-tasks)",
addSubtaskPlaceholder: "하위 할 일 추가…",
deleteTaskConfirm: "이 할 일을 삭제하시겠습니까?",
autoSaved: "자동 저장됨",
saving: "저장 중…",
created: "생성일",
// Toolbars
bold: "굵게",
italic: "기울임",
heading: "제목",
bulletList: "글머리 기호",
numberedList: "번호 매기기",
checkbox: "체크박스",
code: "코드 블록",
},
ja: {
auth: {
welcome: "おかえりなさい",
signIn: "サインイン",
signInSub: "アカウントにサインイン",
createAccount: "アカウント作成",
createAccountSub: "今日からタスクを整理しましょう",
email: "メールアドレス",
password: "パスワード",
passwordHint: "8文字以上",
displayName: "表示名",
namePlaceholder: "あなたの名前",
noAccount: "アカウントをお持ちでない方は",
hasAccount: "すでにアカウントをお持ちですか?",
createOne: "作成する",
signInLink: "サインイン",
signingIn: "サインイン中...",
creating: "アカウント作成中...",
invalidCredentials: "メールアドレスまたはパスワードが正しくありません。",
registrationFailed: "登録に失敗しました。",
},
sidebar: {
lists: "リスト",
newList: "新しいリスト",
create: "作成",
cancel: "キャンセル",
listName: "リスト名",
importTasks: "インポート",
signOut: "サインアウト",
deleteListConfirm: "このリストとすべてのタスクを削除しますか?",
},
tasks: {
addTask: "タスクを追加...",
add: "追加",
showDone: "完了を表示",
hideDone: "完了を非表示",
noTasks: "タスクがありません。下から追加してください!",
selectList: "リストを選択して始めましょう",
completed: "完了済み",
loading: "読み込み中...",
},
detail: {
priority: "優先度",
dueDate: "期限",
notes: "メモ",
notesPlaceholder: "メモ、詳細、覚えておくべきことを追加\u2026\n\nMarkdown: **太字**, *斜体*, # 見出し, - リスト, - [ ] チェックボックス",
subtasks: "サブタスク",
addSubtask: "サブタスクを追加\u2026",
autoSaved: "自動保存済み",
saving: "保存中\u2026",
delete: "削除",
deleteConfirm: "このタスクを削除しますか?",
close: "閉じる",
clear: "クリア",
created: "作成日",
},
priority: { none: "なし", low: "低", medium: "中", high: "高" },
settings: {
theme: "テーマ",
language: "言語",
system: "システム",
light: "ライト",
dark: "ダーク",
},
import: {
title: "タスクをインポート",
targetList: "対象リスト",
file: "ファイル (CSV または ICS)",
tip: "TickTick: 設定 \u2192 エクスポート \u2192 CSVまたはiCalendarでエクスポート",
cancel: "キャンセル",
import: "インポート",
importing: "インポート中...",
error: "エラー",
},
list: { create: "リスト作成", color: "カラー" },
// Auth
appName: "CheckFlow",
tagline: "シンプルなタスク管理",
welcomeBack: "おかえりなさい",
signInSubtitle: "アカウントにサインイン",
createAccount: "アカウント作成",
createAccountSubtitle: "今日からタスクを整理しましょう",
displayName: "お名前",
email: "メールアドレス",
password: "パスワード",
min8Chars: "8文字以上",
signIn: "サインイン",
signingIn: "サインイン中...",
createBtn: "アカウントを作成",
creatingBtn: "作成中...",
alreadyHaveAccount: "すでにアカウントをお持ちですか?",
dontHaveAccount: "アカウントをお持ちでないですか?",
tryDemoMode: "デモモードで試す (DB不要)",
demoBadge: "デモモード",
signOut: "サインアウト",
// Sidebar
lists: "リスト",
newList: "新しいリスト",
listNamePlaceholder: "リスト名",
create: "作成",
cancel: "キャンセル",
deleteListConfirm: "このリストとすべてのタスクを削除しますか?",
importTasks: "タスクのインポート",
importModalTitle: "タスクのインポート",
targetList: "対象リスト",
fileSelectLabel: "ファイル選択 (TickTick CSV または ICS)",
tickTickExportHint: "TickTick: 設定 → エクスポート → CSV または iCalendar",
importBtn: "インポート",
importing: "インポート中...",
theme: "テーマ",
themeSystem: "システム",
themeLight: "ライト",
themeDark: "ダーク",
language: "言語",
// Task List
tasks: "タスク",
hideDone: "完了を非表示",
showDone: "完了を表示",
completedSection: "完了済み",
addTaskPlaceholder: "タスクを追加...",
add: "追加",
noTasksYet: "タスクがまだありません。下から追加してください!",
selectListToStart: "リストを選択してください",
loading: "読み込み中...",
today: "今日",
tomorrow: "明日",
// Task Detail
taskTitlePlaceholder: "タスク名",
priority: "優先度",
priorityNone: "なし",
priorityLow: "",
priorityMedium: "",
priorityHigh: "",
dueDate: "期限",
clear: "クリア",
notes: "メモ",
notesPlaceholder: "詳細やメモを自由に入力してください…\n\nMarkdown対応: **太字**, *斜体*, # 見出し, - リスト, - [ ] チェックボックス",
subtasks: "サブタスク",
addSubtaskPlaceholder: "サブタスクを追加…",
deleteTaskConfirm: "このタスクを削除しますか?",
autoSaved: "自動保存済み",
saving: "保存中…",
created: "作成日時",
// Toolbars
bold: "太字",
italic: "斜体",
heading: "見出し",
bulletList: "箇条書き",
numberedList: "番号付きリスト",
checkbox: "チェックボックス",
code: "コード",
},
} as const;
};
export type Translations = typeof translations.en;
export type TranslationKey = keyof Translations;
// ============================================================
// CONTEXT
// ============================================================
interface I18nContextValue {
locale: Locale;
setLocale: (l: Locale) => void;
t: Translations;
interface I18nContextType {
lang: Language;
setLang: (lang: Language) => void;
t: (key: keyof typeof translations["en"]) => string;
}
const I18nContext = createContext<I18nContextValue>({
locale: "en",
setLocale: () => {},
t: translations.en,
const I18nContext = createContext<I18nContextType>({
lang: "en",
setLang: () => {},
t: (key) => translations.en[key] || (key as string),
});
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocaleState] = useState<Locale>("en");
const [lang, setLangState] = useState<Language>("en");
useEffect(() => {
const saved = localStorage.getItem("locale") as Locale | null;
if (saved && saved in translations) setLocaleState(saved);
const saved = localStorage.getItem("checkflow_lang") as Language;
if (saved && (saved === "en" || saved === "ko" || saved === "ja")) {
setLangState(saved);
} else {
// Default to English as requested, or match browser if preferred
const navLang = navigator.language.toLowerCase();
if (navLang.startsWith("ko")) setLangState("ko");
else if (navLang.startsWith("ja")) setLangState("ja");
else setLangState("en");
}
}, []);
const setLocale = (l: Locale) => {
setLocaleState(l);
localStorage.setItem("locale", l);
const setLang = (newLang: Language) => {
setLangState(newLang);
localStorage.setItem("checkflow_lang", newLang);
};
const t = (key: keyof typeof translations["en"]): string => {
return translations[lang]?.[key] || translations.en[key] || (key as string);
};
return (
<I18nContext.Provider value={{ locale, setLocale, t: translations[locale] }}>
<I18nContext.Provider value={{ lang, setLang, t }}>
{children}
</I18nContext.Provider>
);
}
export function useI18n() {
return useContext(I18nContext);
}
export const LOCALES: { value: Locale; label: string; flag: string }[] = [
{ value: "en", label: "English", flag: "🇺🇸" },
{ value: "ko", label: "한국어", flag: "🇰🇷" },
{ value: "ja", label: "日本語", flag: "🇯🇵" },
];
export const useI18n = () => useContext(I18nContext);
+150
View File
@@ -0,0 +1,150 @@
export interface MockList {
id: string;
name: string;
color: string;
icon: string;
_count?: { tasks: number };
}
export interface MockTask {
id: string;
listId: string;
parentId: string | null;
title: string;
note: string | null;
completed: boolean;
completedAt: string | null;
dueDate: string | null;
priority: number;
sortOrder: number;
createdAt: string;
updatedAt: string;
children: MockTask[];
tags: { tag: { id: string; name: string; color: string } }[];
}
const STORAGE_KEY_LISTS = "checkflow_demo_lists";
const STORAGE_KEY_TASKS = "checkflow_demo_tasks";
export const INITIAL_DEMO_LISTS: MockList[] = [
{ id: "list-1", name: "🚀 Project Launch", color: "#4B7BF5", icon: "work" },
{ id: "list-2", name: "🏠 Personal & Daily", color: "#10B981", icon: "personal" },
{ id: "list-3", name: "📚 Tech Research", color: "#8B5CF6", icon: "study" },
];
export const INITIAL_DEMO_TASKS: MockTask[] = [
{
id: "task-1",
listId: "list-1",
parentId: null,
title: "Setup Self-Hosted CheckFlow on NAS",
note: "## 🐳 Docker Deployment Guide\n\n- Deploy with `docker compose up -d`\n- Forward port 3000 to NPM (Nginx Proxy Manager)\n- Setup SSL certificate for custom domain\n\n### 🔗 Key Endpoints\n- Web App: `https://todo.yourdomain.com`\n- CardDAV: `https://todo.yourdomain.com/api/dav`",
completed: false,
completedAt: null,
dueDate: new Date(Date.now() + 86400000).toISOString(),
priority: 3,
sortOrder: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
children: [
{
id: "sub-1-1",
listId: "list-1",
parentId: "task-1",
title: "Configure .env environment variables",
note: null,
completed: true,
completedAt: new Date().toISOString(),
dueDate: null,
priority: 0,
sortOrder: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
children: [],
tags: [],
},
{
id: "sub-1-2",
listId: "list-1",
parentId: "task-1",
title: "Test DAVx⁵ sync on Samsung Galaxy",
note: null,
completed: false,
completedAt: null,
dueDate: null,
priority: 2,
sortOrder: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
children: [],
tags: [],
},
],
tags: [],
},
{
id: "task-2",
listId: "list-1",
parentId: null,
title: "Import existing tasks from TickTick",
note: "1. Export CSV/ICS in TickTick Settings\n2. Click 'Import Tasks' in CheckFlow Sidebar\n3. Select target project",
completed: false,
completedAt: null,
dueDate: null,
priority: 2,
sortOrder: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
children: [],
tags: [],
},
{
id: "task-3",
listId: "list-2",
parentId: null,
title: "Install CheckFlow PWA on mobile home screen",
note: "Open in Chrome/Samsung Internet -> Menu -> **Install app** or **Add to Home Screen**",
completed: true,
completedAt: new Date().toISOString(),
dueDate: new Date().toISOString(),
priority: 1,
sortOrder: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
children: [],
tags: [],
},
];
export function getDemoStore() {
if (typeof window === "undefined") {
return { lists: INITIAL_DEMO_LISTS, tasks: INITIAL_DEMO_TASKS };
}
let lists: MockList[] = INITIAL_DEMO_LISTS;
let tasks: MockTask[] = INITIAL_DEMO_TASKS;
try {
const savedLists = localStorage.getItem(STORAGE_KEY_LISTS);
if (savedLists) lists = JSON.parse(savedLists);
else localStorage.setItem(STORAGE_KEY_LISTS, JSON.stringify(INITIAL_DEMO_LISTS));
const savedTasks = localStorage.getItem(STORAGE_KEY_TASKS);
if (savedTasks) tasks = JSON.parse(savedTasks);
else localStorage.setItem(STORAGE_KEY_TASKS, JSON.stringify(INITIAL_DEMO_TASKS));
} catch (e) {
console.error("Failed to read demo storage", e);
}
return { lists, tasks };
}
export function saveDemoStore(lists: MockList[], tasks: MockTask[]) {
if (typeof window === "undefined") return;
try {
localStorage.setItem(STORAGE_KEY_LISTS, JSON.stringify(lists));
localStorage.setItem(STORAGE_KEY_TASKS, JSON.stringify(tasks));
} catch (e) {
console.error("Failed to save demo storage", e);
}
}