Detect locale with is-china-user and show English copy for non-Chinese visitors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric
2026-08-03 08:54:39 -05:00
parent 4c2101a87a
commit af0a430864
9 changed files with 233 additions and 20 deletions

View File

@@ -0,0 +1,75 @@
"use client";
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import { isChinaUser } from "is-china-user";
import { dictionaries, type Dictionary, type Locale } from "~/lib/i18n";
type LocaleContextValue = {
locale: Locale;
isChina: boolean;
ready: boolean;
t: Dictionary;
};
const LocaleContext = createContext<LocaleContextValue | null>(null);
export function LocaleProvider({ children }: { children: ReactNode }) {
// Default zh to match primary audience; switch after client detection.
const [locale, setLocale] = useState<Locale>("zh");
const [ready, setReady] = useState(false);
useEffect(() => {
let cancelled = false;
try {
const china = Boolean(isChinaUser());
if (!cancelled) {
setLocale(china ? "zh" : "en");
setReady(true);
}
} catch {
if (!cancelled) {
setLocale("zh");
setReady(true);
}
}
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (!ready) return;
document.documentElement.lang = dictionaries[locale].htmlLang;
}, [locale, ready]);
const value = useMemo<LocaleContextValue>(
() => ({
locale,
isChina: locale === "zh",
ready,
t: dictionaries[locale],
}),
[locale, ready],
);
return (
<LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
);
}
export function useLocale() {
const ctx = useContext(LocaleContext);
if (!ctx) {
throw new Error("useLocale must be used within LocaleProvider");
}
return ctx;
}

View File

@@ -3,6 +3,7 @@
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { ENTRY_CARDS } from "~/lib/data";
import { useLocale } from "~/components/locale-provider";
const CARD_STYLE = {
blue: {
@@ -23,6 +24,8 @@ const CARD_STYLE = {
} as const;
export function Hero() {
const { t } = useLocale();
return (
<section
className="flex flex-1 flex-col justify-center px-5 pb-8"
@@ -32,25 +35,29 @@ export function Hero() {
}}
>
<div className="mx-auto w-full max-w-md sm:max-w-lg">
{/* 标题 */}
<div className="text-center">
<h1 className="text-[2rem] font-bold leading-tight sm:text-5xl">
<span className="text-gradient">数字游民</span>
<span className="text-gradient">{t.brandSubtitle}</span>
</h1>
<p className="mt-2 text-lg font-medium tracking-[0.2em] text-neutral-400 sm:text-xl">
nomadro
</p>
<p className="mx-auto mt-4 max-w-xs text-[13px] leading-relaxed text-neutral-500 sm:max-w-sm sm:text-sm">
游民导航 · 地理套利 · 直播工具箱
{t.heroTagline}
<br />
一站开启自由生活
{t.heroTaglineLine2}
</p>
</div>
{/* 入口卡片 */}
<div className="mt-10 flex flex-col gap-3">
{ENTRY_CARDS.map((card) => {
const style = CARD_STYLE[card.accent];
const copy = t.entryCards[card.id] ?? {
label: card.label,
title: card.title,
description: card.description,
};
return (
<Link
key={card.id}
@@ -72,19 +79,19 @@ export function Hero() {
<div className="min-w-0 flex-1 pr-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
<h2 className="text-[15px] font-semibold text-neutral-900 sm:text-base">
{card.label}
{copy.label}
</h2>
{card.id === "live" && (
<span className="rounded bg-rose-500 px-1.5 py-px text-[10px] font-semibold leading-4 text-white">
LIVE
{t.liveBadge}
</span>
)}
</div>
<p className="mt-0.5 text-sm font-medium text-neutral-700">
{card.title}
{copy.title}
</p>
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-neutral-400">
{card.description}
{copy.description}
</p>
</div>