diff --git a/app/[username]/vs/[opponent]/page.tsx b/app/[username]/vs/[opponent]/page.tsx index 4f45375..da68a91 100644 --- a/app/[username]/vs/[opponent]/page.tsx +++ b/app/[username]/vs/[opponent]/page.tsx @@ -9,6 +9,7 @@ import { getRepoStars } from "@/lib/github/stars"; import { pickFlag } from "@/lib/flagPriority"; import { recordScout } from "@/lib/analytics"; import { computeDuel } from "@/lib/duel"; +import { resolveServerLocale } from "@/lib/i18n/server"; import type { Card } from "@/lib/scoring/types"; export const dynamic = "force-dynamic"; // per-user, token-gated, always fresh @@ -120,7 +121,8 @@ export default async function Page({ params }: Params) { after(() => Promise.all([recordScout(), recordScout()])); // a duel is two scouts - const duel = computeDuel(withFlag(a.card), withFlag(b.card)); + const locale = await resolveServerLocale(); + const duel = computeDuel(withFlag(a.card), withFlag(b.card), locale); return (
diff --git a/app/error.tsx b/app/error.tsx index bc2e79a..5c18ebd 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -3,6 +3,8 @@ import { useEffect } from "react"; import Link from "next/link"; import Background from "@/components/Background"; +import { useLocale } from "@/lib/i18n/useLocale"; +import { useUIDict } from "@/lib/i18n/hooks"; // Route-level error boundary. Catches an unexpected throw in the route subtree — // a bad render, or an upstream failure that isn't a clean GithubError the page @@ -16,27 +18,35 @@ export default function Error({ error, reset }: { error: Error & { digest?: stri console.error("[gitfut] route error:", error); }, [error]); + const locale = useLocale(); + const ui = useUIDict().errors; + const errKicker = ui.errorKicker; + const errTitle = ui.errorTitle; + const errBody = ui.errorBody; + const tryAgain = locale === "ko" ? "다시 시도" : "TRY AGAIN"; + const home = locale === "ko" ? "홈" : "HOME"; + return (
-
SCOUT REPORT
-

The scout hit a snag

+
{errKicker}
+

{errTitle}

- Something broke mid-scout. Try again — if it keeps happening, the pitch may be down for a moment. + {errBody}

- HOME + {home}
{error.digest &&

ref: {error.digest}

} diff --git a/app/global-error.tsx b/app/global-error.tsx index 7f2c01d..7a456f4 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,6 +1,8 @@ "use client"; import { useEffect } from "react"; +import { useLocale } from "@/lib/i18n/useLocale"; +import { useUIDict } from "@/lib/i18n/hooks"; // Last-resort boundary: a throw in the root layout itself escapes app/error.tsx, // so this replaces the entire document (it must render its own /). @@ -18,8 +20,15 @@ export default function GlobalError({ console.error("[gitfut] global error:", error); }, [error]); + // GlobalError renders before the LocaleProvider mounts, so useLocale() may + // not have a value. Fall back to ENGLISH silently — global errors are rare + // and the user can still resolve the message. + const locale = useLocale(); + const ui = useUIDict().errors; + void locale; + return ( - +
GITFUT
-

Match abandoned

+

{ui.globalTitle}

- Something went badly wrong. Reload to get back to the pitch. + {ui.globalBody}

{error.digest && (

{ + // Title/description are pinned to EN for SEO crawlers — search engines + // index the EN copy and ignore the user's locale. Switching to per-locale + // metadata would require for the homepage which is + // not part of this change. + const en = getDict(DEFAULT_LOCALE); + const TITLE = `${en.meta.brand} — ${en.meta.tagline}`; + const DESCRIPTION = + "Rate any GitHub profile out of 99 as a FIFA-Ultimate-Team-style player card, scored from real commits, stars and contributions. Get scouted and share your card."; + return { + metadataBase: new URL("https://gitfut.com"), title: TITLE, description: DESCRIPTION, - }, -}; + keywords: [ + "GitHub profile card", + "rate my GitHub", + "GitHub stats", + "developer trading card", + "FUT card", + "GitHub rating", + "World Cup", + "GitFut", + ], + alternates: { canonical: "/" }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: "https://gitfut.com", + siteName: "GitFut", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: TITLE, + description: DESCRIPTION, + }, + }; +} export const viewport: Viewport = { themeColor: "#02001e", }; -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default async function RootLayout({ children }: { children: React.ReactNode }) { + // Resolve locale on the server (cookie > Accept-Language > default) so the + // initial markup matches the user's locale — prevents the visible "EN briefly + // then KO" flash on first paint. + const cookieStore = await cookies(); + const locale = localeFromCookie(cookieStore.get(LOCALE_COOKIE_NAME)?.value); + return ( - {children} + {children}