feat(ux): dark/light theme + en/ja i18n + user preferences

UX全体にテーマと言語設定を追加。

- lib/db/migrations/004_user_prefs.sql: users に theme/locale 列を追加(既定 dark/en)
- lib/i18n/: dictionary(en/ja) + I18nProvider(クライアントcontext: locale/theme/t/setLocale/setTheme) + server.ts(SSR用 getLocale/getTheme/translate) + constants.ts
- app/layout.tsx: theme/locale Cookie を読み <html class/lang> をSSR、I18nProvider でラップ(フラッシュなし)
- tailwind darkMode:'class' + 全画面に dark: バリアントを一括付与(Nodeスクリプト lookbehind で安全に変換)
- components/layout/ThemeToggle: 即時クラス切替+Cookie+永続化
- app/profile: テーマ/言語セレクタ、chrome翻訳(Header/ProjectNav/login/dashboard/profile)
- PATCH /api/users/me: theme/locale を受理(バリデーション→400)、Cookieを設定
- E2E: locale=ja storageState で既存JAアサーションを維持、theme-i18n.spec で既定en/darkと切替を検証
This commit is contained in:
Ken Yasue
2026-06-25 11:37:44 +02:00
parent 6a134a29bd
commit 921cdc457d
74 changed files with 1092 additions and 279 deletions

172
lib/i18n/dictionary.ts Normal file
View File

@ -0,0 +1,172 @@
import type { Locale } from '@/lib/types';
/**
* 多言語辞書。en/ja の両方に同じキーを定義する。
* 共通クロム(ヘッダー/ナビ)、認証、プロフィール/設定、ページタイトル、
* 共通アクションラベルをカバーする。機能画面の本文は漸次追加する。
*/
const en = {
'app.name': 'Groupware',
'nav.dashboard': 'Dashboard',
'nav.overview': 'Overview',
'nav.board': 'Board',
'nav.notes': 'Notes',
'nav.chat': 'Chat',
'nav.todos': 'ToDo',
'nav.files': 'Files',
'nav.calendar': 'Calendar',
'nav.milestones': 'Milestones',
'nav.meetings': 'Meetings',
'nav.search': 'Search',
'nav.members': 'Members',
'nav.activity': 'Activity',
'nav.settings': 'Settings',
'header.logout': 'Logout',
'common.save': 'Save',
'common.cancel': 'Cancel',
'common.create': 'Create',
'common.delete': 'Delete',
'common.submit': 'Submit',
'common.loading': 'Loading...',
'common.back': 'Back',
'common.edit': 'Edit',
'theme.dark': 'Dark',
'theme.light': 'Light',
'language.english': 'English',
'language.japanese': '日本語',
'auth.login': 'Login',
'auth.register': 'Register',
'auth.displayName': 'Display name',
'auth.email': 'Email',
'auth.password': 'Password',
'auth.registerHere': 'New here? Register',
'auth.registerButton': 'Register',
'auth.loginButton': 'Login',
'auth.backToLogin': 'Back to login',
'auth.processing': 'Processing...',
'auth.failed': 'Operation failed',
'auth.projectName': 'Project name',
'auth.newProject': 'New project',
'project.description': 'Description (optional)',
'project.creating': 'Creating...',
'project.createFailed': 'Failed to create project',
'profile.title': 'Profile',
'profile.displayName': 'Display name',
'profile.email': 'Email',
'profile.avatarUrl': 'Avatar image URL',
'profile.saved': 'Profile updated',
'profile.backToDashboard': 'Back to dashboard',
'profile.role': 'Role',
'profile.theme': 'Theme',
'profile.language': 'Language',
'page.dashboard': 'Dashboard',
'page.board': 'Board',
'page.notes': 'Notes',
'page.chat': 'Chat',
'page.todos': 'ToDo / Kanban',
'page.files': 'Files',
'page.calendar': 'Calendar',
'page.milestones': 'Milestones',
'page.meetings': 'Meetings',
'page.search': 'Search',
'page.members': 'Members',
'page.activity': 'Activity',
'page.settings': 'Settings',
'page.profile': 'Profile',
'page.login': 'Login',
'dash.projects': 'Joined projects',
'dash.unreadNotifications': 'Unread notifications',
'dash.incompleteTodos': 'Incomplete ToDo',
'dash.overdueTasks': 'Overdue tasks',
'dash.upcomingMeetings': 'Upcoming meetings',
'dash.recentActivity': 'Recent activity',
'dash.empty': 'None',
'dash.noUnread': 'No unread',
'dash.openNotifications': 'Open notifications',
'dash.due': 'Due',
};
const ja: typeof en = {
'app.name': 'シンプルグループウェア',
'nav.dashboard': 'ダッシュボード',
'nav.overview': '概要',
'nav.board': '掲示板',
'nav.notes': 'メモ',
'nav.chat': 'チャット',
'nav.todos': 'ToDo',
'nav.files': 'ファイル',
'nav.calendar': 'カレンダー',
'nav.milestones': 'マイルストーン',
'nav.meetings': 'ミーティング',
'nav.search': '検索',
'nav.members': 'メンバー',
'nav.activity': 'アクティビティ',
'nav.settings': '設定',
'header.logout': 'ログアウト',
'common.save': '保存',
'common.cancel': 'キャンセル',
'common.create': '作成',
'common.delete': '削除',
'common.submit': '送信',
'common.loading': '読み込み中...',
'common.back': '戻る',
'common.edit': '編集',
'theme.dark': 'ダーク',
'theme.light': 'ライト',
'language.english': 'English',
'language.japanese': '日本語',
'auth.login': 'ログイン',
'auth.register': '新規登録',
'auth.displayName': '表示名',
'auth.email': 'メールアドレス',
'auth.password': 'パスワード',
'auth.registerHere': '新規登録はこちら',
'auth.registerButton': '登録する',
'auth.loginButton': 'ログイン',
'auth.backToLogin': 'ログイン画面に戻る',
'auth.processing': '処理中...',
'auth.failed': '処理に失敗しました',
'auth.projectName': 'プロジェクト名',
'auth.newProject': '新規プロジェクト',
'project.description': '説明(任意)',
'project.creating': '作成中...',
'project.createFailed': 'プロジェクトの作成に失敗しました',
'profile.title': 'プロフィール',
'profile.displayName': '表示名',
'profile.email': 'メールアドレス',
'profile.avatarUrl': 'アイコン画像URL',
'profile.saved': 'プロフィールを更新しました',
'profile.backToDashboard': 'ダッシュボードへ',
'profile.role': 'ロール',
'profile.theme': 'テーマ',
'profile.language': '言語',
'page.dashboard': 'ダッシュボード',
'page.board': '掲示板',
'page.notes': 'メモ',
'page.chat': 'チャット',
'page.todos': 'ToDo / Kanban',
'page.files': 'ファイル',
'page.calendar': 'カレンダー',
'page.milestones': 'マイルストーン',
'page.meetings': 'ミーティング',
'page.search': '検索',
'page.members': 'メンバー',
'page.activity': 'アクティビティ',
'page.settings': '設定',
'page.profile': 'プロフィール',
'page.login': 'ログイン',
'dash.projects': '参加プロジェクト',
'dash.unreadNotifications': '未読通知',
'dash.incompleteTodos': '未完了ToDo',
'dash.overdueTasks': '期限切れタスク',
'dash.upcomingMeetings': '近日中のミーティング',
'dash.recentActivity': '最近のアクティビティ',
'dash.empty': 'ありません',
'dash.noUnread': '未読はありません',
'dash.openNotifications': '通知一覧を開く',
'dash.due': '期限',
};
export const dictionary: Record<Locale, typeof en> = { en, ja };
export type MessageKey = keyof typeof en;