diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 0000000..498c484 --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,67 @@ +import { Link } from 'react-router'; +import { SupportedLanguage, saveLanguage } from '../locales'; + +interface HeaderProps { + currentLang: SupportedLanguage; +} + +export function Header({ currentLang }: HeaderProps) { + const handleLanguageSwitch = (lang: SupportedLanguage) => { + saveLanguage(lang); + // Set cookie for server-side detection + document.cookie = `lang=${lang}; path=/; max-age=31536000`; // 1 year + }; + + return ( +
+ + Bug Sniper + +
+ {/* Language switcher */} +
+ handleLanguageSwitch('ja')} + className={`text-2xl transition-opacity hover:opacity-100 ${ + currentLang === 'ja' ? 'opacity-100' : 'opacity-50' + }`} + aria-label="日本語" + title="日本語" + > + 🇯🇵 + + handleLanguageSwitch('en')} + className={`text-2xl transition-opacity hover:opacity-100 ${ + currentLang === 'en' ? 'opacity-100' : 'opacity-50' + }`} + aria-label="English" + title="English" + > + 🇬🇧 + +
+ + {/* GitHub link */} + + + + + +
+
+ ); +} diff --git a/app/locales/en.json b/app/locales/en.json index dc5eac3..107457f 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -1,11 +1,11 @@ { "title": "Bug Sniper", + "catchphrase": "Find bugs for 60 seconds!", "button.start": "Start", "button.skip": "Skip", "label.score": "Score", "label.time": "Time", "label.combo": "Combo", - "label.uiLanguage": "UI Language", "label.codeLanguage": "Code Language", "nav.ranking": "Ranking", "nav.home": "Home", @@ -14,5 +14,10 @@ "language.php": "PHP", "language.ruby": "Ruby", "language.java": "Java", - "language.dart": "Dart" + "language.dart": "Dart", + "rules.title": "Rules", + "rules.time": "60 seconds time limit", + "rules.tap": "Tap lines with bugs to report them", + "rules.combo": "Consecutive correct answers for combo bonus", + "rules.skip": "Skip difficult problems" } diff --git a/app/locales/ja.json b/app/locales/ja.json index f83ed8f..56bfd01 100644 --- a/app/locales/ja.json +++ b/app/locales/ja.json @@ -1,11 +1,11 @@ { "title": "Bug Sniper", + "catchphrase": "60秒間バグを見つけ続けろ!", "button.start": "スタート", "button.skip": "スキップ", "label.score": "スコア", "label.time": "残り時間", "label.combo": "コンボ", - "label.uiLanguage": "UI言語", "label.codeLanguage": "コード言語", "nav.ranking": "ランキング", "nav.home": "ホーム", @@ -14,5 +14,10 @@ "language.php": "PHP", "language.ruby": "Ruby", "language.java": "Java", - "language.dart": "Dart" + "language.dart": "Dart", + "rules.title": "ルール", + "rules.time": "制限時間は60秒", + "rules.tap": "バグがある行をタップして指摘", + "rules.combo": "連続正解でコンボボーナス", + "rules.skip": "難しい問題はスキップ可能" } diff --git a/app/routes.ts b/app/routes.ts index 205ff3c..11e6cf5 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,3 +1,6 @@ -import { type RouteConfig, index } from '@react-router/dev/routes'; +import { type RouteConfig, index, route } from '@react-router/dev/routes'; -export default [index('routes/home.tsx')] satisfies RouteConfig; +export default [ + index('routes/_index.tsx'), + route(':lang', 'routes/$lang.tsx'), +] satisfies RouteConfig; diff --git a/app/routes/$lang.tsx b/app/routes/$lang.tsx new file mode 100644 index 0000000..4e68af7 --- /dev/null +++ b/app/routes/$lang.tsx @@ -0,0 +1,44 @@ +import { redirect } from 'react-router'; +import type { Route } from './+types/$lang'; +import { SupportedLanguage } from '../locales'; +import { Welcome } from '../welcome/welcome'; +import { Header } from '../components/Header'; + +/** + * Language-specific home route + */ +export function meta({ params }: Route.MetaArgs) { + const lang = params.lang as SupportedLanguage; + return [ + { title: 'Bug Sniper' }, + { + name: 'description', + content: + lang === 'ja' + ? 'コードレビューゲーム - バグを見つけてスコアを競おう!' + : 'Code review game - Find bugs and compete for high scores!', + }, + ]; +} + +export function loader({ params }: Route.LoaderArgs) { + const lang = params.lang; + + // Validate language parameter + if (lang !== 'ja' && lang !== 'en') { + throw redirect('/'); + } + + return { + lang: lang as SupportedLanguage, + }; +} + +export default function LanguageHome({ loaderData }: Route.ComponentProps) { + return ( + <> +
+ + + ); +} diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx new file mode 100644 index 0000000..66f6028 --- /dev/null +++ b/app/routes/_index.tsx @@ -0,0 +1,37 @@ +import { redirect } from 'react-router'; +import type { Route } from './+types/_index'; + +/** + * Root index route - detects browser language and redirects to appropriate language route + */ +export function loader({ request }: Route.LoaderArgs) { + const url = new URL(request.url); + + // Check if there's a lang cookie + const cookieHeader = request.headers.get('Cookie'); + let langFromCookie: string | null = null; + + if (cookieHeader) { + const cookies = cookieHeader.split(';').map(c => c.trim()); + const langCookie = cookies.find(c => c.startsWith('lang=')); + if (langCookie) { + langFromCookie = langCookie.split('=')[1]; + } + } + + // Determine language: cookie > Accept-Language header + let lang = 'en'; + + if (langFromCookie === 'ja' || langFromCookie === 'en') { + lang = langFromCookie; + } else { + // Check Accept-Language header + const acceptLanguage = request.headers.get('Accept-Language'); + if (acceptLanguage && acceptLanguage.toLowerCase().includes('ja')) { + lang = 'ja'; + } + } + + // Redirect to language-specific route + return redirect(`/${lang}`); +} diff --git a/app/welcome/welcome.tsx b/app/welcome/welcome.tsx index c39e0f4..5f66ffc 100644 --- a/app/welcome/welcome.tsx +++ b/app/welcome/welcome.tsx @@ -1,39 +1,55 @@ -import logoDark from './logo-dark.svg'; -import logoLight from './logo-light.svg'; +import { SupportedLanguage, t } from '../locales'; -export function Welcome({ message }: { message: string }) { +export function Welcome({ lang }: { lang: SupportedLanguage }) { return ( -
-
-
-
- React Router - React Router -
-
-
- +
+ +
+ + +
+ +
);