-
Notifications
You must be signed in to change notification settings - Fork 0
Add multilingual support with language switcher #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
goofmint
merged 4 commits into
main
from
claude/add-multilingual-support-01NbfyeonJ12qUM9hS44foLj
Nov 21, 2025
+215
−38
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
87ceffe
feat: Implement multilingual support (i18n) with Japanese and English
claude 1e829ca
fix: Add missing routes.ts changes for language routing
claude ae1188b
feat: Improve home page with catchphrase and game rules
claude 66a7129
refactor: Remove unused useLocation import from Header
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <header className="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 backdrop-blur"> | ||
| <Link to={`/${currentLang}`} className="text-xl font-bold tracking-tight"> | ||
| Bug Sniper | ||
| </Link> | ||
| <div className="flex items-center space-x-4"> | ||
| {/* Language switcher */} | ||
| <div className="flex items-center space-x-2"> | ||
| <Link | ||
| to="/ja" | ||
| onClick={() => handleLanguageSwitch('ja')} | ||
| className={`text-2xl transition-opacity hover:opacity-100 ${ | ||
| currentLang === 'ja' ? 'opacity-100' : 'opacity-50' | ||
| }`} | ||
| aria-label="日本語" | ||
| title="日本語" | ||
| > | ||
| 🇯🇵 | ||
| </Link> | ||
| <Link | ||
| to="/en" | ||
| onClick={() => handleLanguageSwitch('en')} | ||
| className={`text-2xl transition-opacity hover:opacity-100 ${ | ||
| currentLang === 'en' ? 'opacity-100' : 'opacity-50' | ||
| }`} | ||
| aria-label="English" | ||
| title="English" | ||
| > | ||
| 🇬🇧 | ||
| </Link> | ||
| </div> | ||
|
|
||
| {/* GitHub link */} | ||
| <a | ||
| href="https://github.com/goofmint/BugSniper" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| aria-label="GitHub" | ||
| className="text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors" | ||
| > | ||
| <svg | ||
| className="w-6 h-6" | ||
| fill="currentColor" | ||
| viewBox="0 0 24 24" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" /> | ||
| </svg> | ||
| </a> | ||
| </div> | ||
| </header> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <> | ||
| <Header currentLang={loaderData.lang} /> | ||
| <Welcome lang={loaderData.lang} /> | ||
| </> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}`); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsafe type assertion in meta function.
Line 11 performs an unsafe type assertion of
params.langtoSupportedLanguagewithout validation. Since themetafunction runs before or alongside theloader, the validation in the loader (lines 28-29) does not protect the meta function. If a user accesses an invalid language route (e.g.,/fr), the meta function will cast'fr'toSupportedLanguage, potentially causing type confusion.Apply this diff to add validation to the meta function:
export function meta({ params }: Route.MetaArgs) { - const lang = params.lang as SupportedLanguage; + const lang = params.lang; + + // Use default language for invalid params + const validLang: SupportedLanguage = (lang === 'ja' || lang === 'en') ? lang : 'en'; + return [ { title: 'Bug Sniper' }, { name: 'description', content: - lang === 'ja' + validLang === 'ja' ? 'コードレビューゲーム - バグを見つけてスコアを競おう!' : 'Code review game - Find bugs and compete for high scores!', }, ]; }📝 Committable suggestion
🤖 Prompt for AI Agents