From 87ceffe5ab34856c99685c11be69cba5f7a06ce2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 11:54:04 +0000 Subject: [PATCH 1/4] feat: Implement multilingual support (i18n) with Japanese and English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements the multilingual foundation for Bug Sniper, allowing users to switch between Japanese and English languages. Changes: - Added language-specific routing (/ja, /en) - Created Header component with language switcher using flag emojis (🇯🇵/🇬🇧) - Implemented browser language detection with fallback to Accept-Language header - Added cookie-based language persistence - Updated routing structure to support dynamic language paths - Modified Welcome component to display UI in selected language - Root path (/) now redirects to appropriate language route based on browser settings The implementation follows BASIC.md specification: - Default language is determined by browser settings - Japanese redirects to /ja, English to /en - Language can be switched via header flag icons --- app/components/Header.tsx | 69 +++++++++++++++++++++++++++++++++++++++ app/routes/$lang.tsx | 45 +++++++++++++++++++++++++ app/routes/_index.tsx | 37 +++++++++++++++++++++ app/welcome/welcome.tsx | 69 +++++++++++++++++++++------------------ 4 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 app/components/Header.tsx create mode 100644 app/routes/$lang.tsx create mode 100644 app/routes/_index.tsx diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 0000000..c1bde92 --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,69 @@ +import { Link, useLocation } from 'react-router'; +import { SupportedLanguage, saveLanguage } from '../locales'; + +interface HeaderProps { + currentLang: SupportedLanguage; +} + +export function Header({ currentLang }: HeaderProps) { + const location = useLocation(); + + 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/routes/$lang.tsx b/app/routes/$lang.tsx new file mode 100644 index 0000000..f7a2db2 --- /dev/null +++ b/app/routes/$lang.tsx @@ -0,0 +1,45 @@ +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, context }: Route.LoaderArgs) { + const lang = params.lang; + + // Validate language parameter + if (lang !== 'ja' && lang !== 'en') { + throw redirect('/'); + } + + return { + lang: lang as SupportedLanguage, + message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE, + }; +} + +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..1d5a869 100644 --- a/app/welcome/welcome.tsx +++ b/app/welcome/welcome.tsx @@ -1,39 +1,44 @@ -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({ message, lang }: { message: string; lang: SupportedLanguage }) { return ( -
-
-
-
- React Router - React Router +
+
+

{t('title', lang)}

+ +
+
+ + +
+ +
+ +
-
-
-
+ + + + {message && ( +

{message}

+ )}
); From 1e829ca1dd620b686babb58b2aea94c16c8245e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 11:55:03 +0000 Subject: [PATCH 2/4] fix: Add missing routes.ts changes for language routing Update routes configuration to support dynamic language paths. This file was inadvertently not included in the previous commit. --- app/routes.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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; From ae1188bcc4deb87f9a4e1b620bd5a6bdbe75c252 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 12:06:25 +0000 Subject: [PATCH 3/4] feat: Improve home page with catchphrase and game rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the home page to provide a better user experience: Changes: - Added catchphrase below title: "60秒間バグを見つけ続けろ!" (Find bugs for 60 seconds!) - Added game rules section with clear instructions - Removed UI language selector (language switching now done via header) - Removed Cloudflare debug message - Simplified Welcome component props by removing unused message parameter - Updated locale files (ja.json, en.json) with new text keys The home page now provides clear information about game rules before users start playing. --- app/locales/en.json | 9 +++++++-- app/locales/ja.json | 9 +++++++-- app/routes/$lang.tsx | 5 ++--- app/welcome/welcome.tsx | 45 +++++++++++++++++++++++++---------------- 4 files changed, 44 insertions(+), 24 deletions(-) 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/$lang.tsx b/app/routes/$lang.tsx index f7a2db2..4e68af7 100644 --- a/app/routes/$lang.tsx +++ b/app/routes/$lang.tsx @@ -21,7 +21,7 @@ export function meta({ params }: Route.MetaArgs) { ]; } -export function loader({ params, context }: Route.LoaderArgs) { +export function loader({ params }: Route.LoaderArgs) { const lang = params.lang; // Validate language parameter @@ -31,7 +31,6 @@ export function loader({ params, context }: Route.LoaderArgs) { return { lang: lang as SupportedLanguage, - message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE, }; } @@ -39,7 +38,7 @@ export default function LanguageHome({ loaderData }: Route.ComponentProps) { return ( <>
- + ); } diff --git a/app/welcome/welcome.tsx b/app/welcome/welcome.tsx index 1d5a869..5f66ffc 100644 --- a/app/welcome/welcome.tsx +++ b/app/welcome/welcome.tsx @@ -1,22 +1,37 @@ import { SupportedLanguage, t } from '../locales'; -export function Welcome({ message, lang }: { message: string; lang: SupportedLanguage }) { +export function Welcome({ lang }: { lang: SupportedLanguage }) { return (
-

{t('title', lang)}

+
+

{t('title', lang)}

+

+ {t('catchphrase', lang)} +

+
-
-
- - +
+
+

{t('rules.title', lang)}

+
    +
  • + + {t('rules.time', lang)} +
  • +
  • + + {t('rules.tap', lang)} +
  • +
  • + + {t('rules.combo', lang)} +
  • +
  • + + {t('rules.skip', lang)} +
  • +
@@ -35,10 +50,6 @@ export function Welcome({ message, lang }: { message: string; lang: SupportedLan - - {message && ( -

{message}

- )}
); From 66a712945a78e52b65d865d571a877371c9155ba Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 12:09:11 +0000 Subject: [PATCH 4/4] refactor: Remove unused useLocation import from Header --- app/components/Header.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/components/Header.tsx b/app/components/Header.tsx index c1bde92..498c484 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -1,4 +1,4 @@ -import { Link, useLocation } from 'react-router'; +import { Link } from 'react-router'; import { SupportedLanguage, saveLanguage } from '../locales'; interface HeaderProps { @@ -6,8 +6,6 @@ interface HeaderProps { } export function Header({ currentLang }: HeaderProps) { - const location = useLocation(); - const handleLanguageSwitch = (lang: SupportedLanguage) => { saveLanguage(lang); // Set cookie for server-side detection