diff --git a/packages/core/src/components/featured-tools.tsx b/packages/core/src/components/featured-tools.tsx index 2ba5ce0..2dc77af 100644 --- a/packages/core/src/components/featured-tools.tsx +++ b/packages/core/src/components/featured-tools.tsx @@ -1,6 +1,6 @@ "use client"; -import Link from "next/link"; +import { LocaleLink } from "@/components/locale-link"; import { useLocale, useTranslations } from "next-intl"; import { ExternalLink, ArrowRight } from "lucide-react"; import { Heading } from "./heading"; @@ -54,9 +54,9 @@ export function FeaturedTools({ tools, categories }: FeaturedToolsProps) { {categories.length > 0 && (
{categories.slice(0, 6).map((category) => ( - {category.icon && {category.icon}} @@ -64,7 +64,7 @@ export function FeaturedTools({ tools, categories }: FeaturedToolsProps) { ({category.toolCount}) - + ))}
)} @@ -72,9 +72,9 @@ export function FeaturedTools({ tools, categories }: FeaturedToolsProps) { {/* 工具网格 */}
{tools.slice(0, 8).map((tool) => ( -
@@ -103,7 +103,7 @@ export function FeaturedTools({ tools, categories }: FeaturedToolsProps) { {t("visitWebsite")}
)} - + ))}
@@ -117,13 +117,13 @@ export function FeaturedTools({ tools, categories }: FeaturedToolsProps) { {/* 查看更多按钮 */} {tools.length > 0 && (
- {t("viewAll")} - +
)} diff --git a/packages/core/src/components/locale-link.tsx b/packages/core/src/components/locale-link.tsx index 86f8c77..9bd93ad 100644 --- a/packages/core/src/components/locale-link.tsx +++ b/packages/core/src/components/locale-link.tsx @@ -1,32 +1,43 @@ "use client"; +import Link from "next/link"; import { useLocale } from 'next-intl'; -import { Link } from 'next-view-transitions'; +import { defaultLocale } from '@/i18n.config'; -interface LocaleLinkProps { +/** + * # 组件 LocaleLink + * 说明:本地化链接(确定性实现)。 + * href 规则:默认语言(en)无前缀,其他语言带 /{locale} 前缀—— + * 与 middleware 的 localePrefix: 'as-needed' 完全一致, + * 站内链接不再产生 /en/... → 307 → /... 的重定向链(SEO/交互问题)。 + * locale prop 用于显式指定目标语言(语言切换场景),默认取当前 locale。 + */ +export function LocaleLink({ + href, + locale: localeProp, + children, + ...props +}: { href: string; + /** 显式目标语言(可选,默认当前语言) */ + locale?: string; children: React.ReactNode; - className?: string; [key: string]: any; -} - -export function LocaleLink({ href, children, ...props }: LocaleLinkProps) { - const locale = useLocale(); +}) { + const currentLocale = useLocale(); + const target = localeProp ?? currentLocale; if (href.startsWith('http') || href.startsWith('//')) { - return {children}; + return {children}; } const normalizedHref = href.startsWith('/') ? href : `/${href}`; + const prefix = target === defaultLocale ? '' : `/${target}`; + const finalHref = `${prefix}${normalizedHref === '/' ? '' : normalizedHref}`; - if ( - normalizedHref === `/${locale}` || - normalizedHref.startsWith(`/${locale}/`) - ) { - return {children}; - } - - const localizedHref = `/${locale}${normalizedHref === '/' ? '' : normalizedHref}`; - - return {children}; + return ( + + {children} + + ); } diff --git a/packages/core/src/features/admin/components/admin-dashboard.tsx b/packages/core/src/features/admin/components/admin-dashboard.tsx index ffe5a81..5ebfbc1 100644 --- a/packages/core/src/features/admin/components/admin-dashboard.tsx +++ b/packages/core/src/features/admin/components/admin-dashboard.tsx @@ -2,7 +2,7 @@ import { useTranslations } from "next-intl"; import { Users, Activity, Wrench, Inbox, MessageSquare, Vote, BarChart3 } from "lucide-react"; -import Link from "next/link"; +import { LocaleLink } from "@/components/locale-link"; import { useLocale } from "next-intl"; interface AdminDashboardProps { @@ -99,9 +99,9 @@ export function AdminDashboard({ stats, recentUsers }: AdminDashboardProps) { {statCards.map((stat) => { const Icon = stat.icon; return ( -
@@ -115,7 +115,7 @@ export function AdminDashboard({ stats, recentUsers }: AdminDashboardProps) {
- + ); })} @@ -150,12 +150,12 @@ export function AdminDashboard({ stats, recentUsers }: AdminDashboardProps) { ))} - {t("viewAllUsers")} - + diff --git a/packages/core/src/features/admin/components/admin-sidebar.tsx b/packages/core/src/features/admin/components/admin-sidebar.tsx index a6d6a69..c6e6e12 100644 --- a/packages/core/src/features/admin/components/admin-sidebar.tsx +++ b/packages/core/src/features/admin/components/admin-sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import Link from "next/link"; +import { LocaleLink } from "@/components/locale-link"; import { usePathname } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; import { Users, Home, Wrench, FolderTree, Tags, Inbox, ShieldQuestion, MessageSquare, Compass, BarChart3, Vote } from "lucide-react"; @@ -73,13 +73,13 @@ export function AdminSidebar() {
- {t("backToDashboard")} - +
diff --git a/packages/core/src/features/alternatives/components/views.tsx b/packages/core/src/features/alternatives/components/views.tsx index 6153420..df1e432 100644 --- a/packages/core/src/features/alternatives/components/views.tsx +++ b/packages/core/src/features/alternatives/components/views.tsx @@ -123,8 +123,8 @@ export async function AlternativesView({ {[base, ...alternatives].map((tool) => ( - {tool.nameEn === base.nameEn && tool.id === base.id @@ -132,7 +132,7 @@ export async function AlternativesView({ : locale === "zh" && tool.nameZh ? tool.nameZh : tool.nameEn} - + @@ -311,12 +311,12 @@ export async function ComparisonView({ {winnerLabel && ( <> {" "}{tc("leaning", { name: winnerLabel })}{" "} - {winnerLabel} - + . )} @@ -331,14 +331,14 @@ export async function ComparisonView({ Compare - + {nameA} - + - + {nameB} - + diff --git a/packages/core/src/features/auth/components/email-verified-guard.tsx b/packages/core/src/features/auth/components/email-verified-guard.tsx index b774f2d..cc8e55e 100644 --- a/packages/core/src/features/auth/components/email-verified-guard.tsx +++ b/packages/core/src/features/auth/components/email-verified-guard.tsx @@ -1,7 +1,7 @@ "use client"; import * as React from "react"; -import { useRouter } from "next/navigation"; +import { useRouter as useIntlRouter } from "@/i18n/routing"; import { useLocale, useTranslations } from 'next-intl'; import { Mail, AlertCircle } from 'lucide-react'; import { useSession } from "@/lib/auth-client"; @@ -15,7 +15,7 @@ export function EmailVerifiedGuard({ children, requireEmailVerification = true }: EmailVerifiedGuardProps) { - const router = useRouter(); + const router = useIntlRouter(); const session = useSession(); const locale = useLocale(); const t = useTranslations('auth.emailVerifiedGuard'); @@ -24,7 +24,7 @@ export function EmailVerifiedGuard({ React.useEffect(() => { if (!session.isPending && !session.data) { - router.replace(`/${locale}/login`); + router.replace(`/login`); } }, [router, session.data, session.isPending, locale]); diff --git a/packages/core/src/features/auth/components/login-form.tsx b/packages/core/src/features/auth/components/login-form.tsx index 8c32227..df18630 100644 --- a/packages/core/src/features/auth/components/login-form.tsx +++ b/packages/core/src/features/auth/components/login-form.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { useRouter } from "next/navigation"; -import Link from "next/link"; +import { LocaleLink } from "@/components/locale-link"; import { useLocale, useTranslations } from 'next-intl'; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; @@ -97,9 +97,9 @@ export function LoginForm() { footer={

{t('noAccount')}{" "} - + {t('signUpLink')} - +

} socialSlot={ @@ -131,9 +131,9 @@ export function LoginForm() { autoComplete="current-password" />
- + {t('forgotPassword')} - +
); diff --git a/packages/core/src/features/auth/components/session-guard.tsx b/packages/core/src/features/auth/components/session-guard.tsx index 6264a9c..f5f0789 100644 --- a/packages/core/src/features/auth/components/session-guard.tsx +++ b/packages/core/src/features/auth/components/session-guard.tsx @@ -13,7 +13,7 @@ export function SessionGuard({ children }: { children: React.ReactNode }) { React.useEffect(() => { if (!session.isPending && !session.data) { - router.replace(`/${locale}/login`); + router.replace(`/login`); } }, [router, session.data, session.isPending]); diff --git a/packages/core/src/features/auth/components/signup-form.tsx b/packages/core/src/features/auth/components/signup-form.tsx index 38c39b5..89765cc 100644 --- a/packages/core/src/features/auth/components/signup-form.tsx +++ b/packages/core/src/features/auth/components/signup-form.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { useRouter } from "next/navigation"; -import Link from "next/link"; +import { LocaleLink } from "@/components/locale-link"; import { useLocale, useTranslations } from 'next-intl'; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; @@ -60,7 +60,7 @@ export function SignupForm() { // 开发模式(默认)注册即已验证 → 直接回跳原页面/首页;生产跳验证提示页 if (process.env.NEXT_PUBLIC_DEV_SKIP_EMAIL_VERIFY === "false") { - router.push(`/${locale}/check-email`); + router.push(`/check-email`); } else { const redirect = typeof window !== "undefined" @@ -115,9 +115,9 @@ export function SignupForm() { footer={

{t('hasAccount')}{" "} - + {t('signInLink')} - +

} socialSlot={ diff --git a/packages/core/src/features/claim/components/claim-button.tsx b/packages/core/src/features/claim/components/claim-button.tsx index 28969e8..d39d6a6 100644 --- a/packages/core/src/features/claim/components/claim-button.tsx +++ b/packages/core/src/features/claim/components/claim-button.tsx @@ -53,7 +53,7 @@ export function ClaimToolButton({ setOpen(false); } else if (result.error === "UNAUTHORIZED") { router.push( - `/${locale}/login?redirect=${encodeURIComponent(window.location.pathname)}` + `/login?redirect=${encodeURIComponent(window.location.pathname)}` ); } else if (result.error === "PENDING_EXISTS") { toast.info(t("pendingExists")); diff --git a/packages/core/src/features/navigation/components/mobile-navbar.tsx b/packages/core/src/features/navigation/components/mobile-navbar.tsx index 07c6958..ee96978 100644 --- a/packages/core/src/features/navigation/components/mobile-navbar.tsx +++ b/packages/core/src/features/navigation/components/mobile-navbar.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { useMotionValueEvent, useScroll } from "framer-motion"; import { IoIosClose, IoIosMenu } from "react-icons/io"; import { Link } from "next-view-transitions"; +import { LocaleLink } from "@/components/locale-link"; import { Button } from "@/components/button"; import { Logo } from "@/components/Logo"; @@ -66,15 +67,15 @@ export const MobileNavbar = () => {
{marketingNavigationKeys.map((navItem) => (
- setOpen(false)} className="relative block w-full py-2 hover:opacity-70 transition-opacity" > {t(navItem.key)} - +
))}
@@ -95,20 +96,20 @@ export const MobileNavbar = () => {

)}
- setOpen(false)} className="text-[15px] font-medium text-muted-foreground py-2 hover:text-foreground transition-colors" > {t('dashboard')} - - + setOpen(false)} className="text-[15px] font-medium text-muted-foreground py-2 hover:text-foreground transition-colors" > {t('profile')} - +
- setIsOpen(false)} className="flex items-center gap-3 px-4 py-2 text-sm text-muted-foreground hover:bg-hover transition-colors" > {t('navigation.main.dashboard')} - + {isAdmin && ( - setIsOpen(false)} className="flex items-center gap-3 px-4 py-2 text-sm text-muted-foreground hover:bg-hover transition-colors whitespace-nowrap" > {t('Admin.sidebar.title')} - + )} - setIsOpen(false)} className="flex items-center gap-3 px-4 py-2 text-sm text-muted-foreground hover:bg-hover transition-colors" > {t('navigation.main.profile')} - +
diff --git a/packages/core/src/features/tools/components/favorite-button.tsx b/packages/core/src/features/tools/components/favorite-button.tsx index bdfe309..18c8ac7 100644 --- a/packages/core/src/features/tools/components/favorite-button.tsx +++ b/packages/core/src/features/tools/components/favorite-button.tsx @@ -54,7 +54,7 @@ export function FavoritesProvider({ children }: { children: React.ReactNode }) { if (!isAuthenticated) { toast(t("loginRequired")); router.push( - `/${locale}/login?redirect=${encodeURIComponent(window.location.pathname)}` + `/login?redirect=${encodeURIComponent(window.location.pathname)}` ); return; } diff --git a/packages/core/src/features/tools/components/favorite-tools-section.tsx b/packages/core/src/features/tools/components/favorite-tools-section.tsx index 5bf3c0f..00c5669 100644 --- a/packages/core/src/features/tools/components/favorite-tools-section.tsx +++ b/packages/core/src/features/tools/components/favorite-tools-section.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import Link from "next/link"; +import { LocaleLink } from "@/components/locale-link"; import { useLocale, useTranslations } from "next-intl"; import { Heart, Loader2 } from "lucide-react"; @@ -45,19 +45,19 @@ export function FavoriteToolsSection() { ) : tools.length === 0 ? (

{t("empty")}

- {t("browseTools")} - +
) : (
{tools.map((tool) => ( - {tool.logoUrl ? ( @@ -82,7 +82,7 @@ export function FavoriteToolsSection() {

)}
- + ))}
)} diff --git a/packages/core/src/features/tools/components/tools-list.tsx b/packages/core/src/features/tools/components/tools-list.tsx index c1ee13e..9cc2af9 100644 --- a/packages/core/src/features/tools/components/tools-list.tsx +++ b/packages/core/src/features/tools/components/tools-list.tsx @@ -1,8 +1,9 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; +import { LocaleLink } from "@/components/locale-link"; +import { useSearchParams } from "next/navigation"; +import { useRouter as useIntlRouter } from "@/i18n/routing"; import { Search, ExternalLink, ChevronLeft, ChevronRight } from "lucide-react"; import { FavoriteButton } from "@/features/tools/components/favorite-button"; import { VoteButton } from "@/features/voting/components/vote-button"; @@ -89,7 +90,7 @@ export function ToolsList({ useCases = [], currentSort = "newest", }: ToolsListProps) { - const router = useRouter(); + const router = useIntlRouter(); const searchParams = useSearchParams(); const [searchInput, setSearchInput] = useState(currentSearch || ""); @@ -102,7 +103,7 @@ export function ToolsList({ params.delete("search"); } params.delete("page"); - router.push(`/${locale}/tools?${params.toString()}`); + router.push(`/tools?${params.toString()}`); }; const handleCategoryChange = (categorySlug: string | null) => { @@ -113,7 +114,7 @@ export function ToolsList({ params.delete("category"); } params.delete("page"); - router.push(`/${locale}/tools?${params.toString()}`); + router.push(`/tools?${params.toString()}`); }; const handlePricingChange = (pricing: string | null) => { @@ -124,7 +125,7 @@ export function ToolsList({ params.delete("pricing"); } params.delete("page"); - router.push(`/${locale}/tools?${params.toString()}`); + router.push(`/tools?${params.toString()}`); }; const currentUseCase = new URLSearchParams(searchParams.toString()).get("usecase") ?? undefined; @@ -137,7 +138,7 @@ export function ToolsList({ params.set("usecase", slug); } params.delete("page"); - router.push(`/${locale}/tools?${params.toString()}`); + router.push(`/tools?${params.toString()}`); }; const handleSortChange = (sort: string) => { @@ -148,13 +149,13 @@ export function ToolsList({ params.delete("sort"); } params.delete("page"); - router.push(`/${locale}/tools?${params.toString()}`); + router.push(`/tools?${params.toString()}`); }; const handlePageChange = (page: number) => { const params = new URLSearchParams(searchParams.toString()); params.set("page", page.toString()); - router.push(`/${locale}/tools?${params.toString()}`); + router.push(`/tools?${params.toString()}`); }; const getName = (tool: ToolData) => @@ -298,9 +299,9 @@ export function ToolsList({ {/* 工具网格 */}
{tools.map((tool) => ( -
@@ -342,7 +343,7 @@ export function ToolsList({ )}
- + ))}
@@ -353,7 +354,7 @@ export function ToolsList({ {translations.noTools}

diff --git a/packages/core/src/pages/[locale]/check-email/page.tsx b/packages/core/src/pages/[locale]/check-email/page.tsx index c0461f2..b65a070 100644 --- a/packages/core/src/pages/[locale]/check-email/page.tsx +++ b/packages/core/src/pages/[locale]/check-email/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { Mail, ArrowRight, RefreshCcw } from 'lucide-react'; +import { LocaleLink } from "@/components/locale-link"; import Link from 'next/link'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; @@ -109,12 +110,12 @@ export default function CheckEmailPage() { )} {/* Back to Login */} - {t('backToLogin')} - + {/* Security Note */} diff --git a/packages/core/src/pages/[locale]/verify-email/page.tsx b/packages/core/src/pages/[locale]/verify-email/page.tsx index b72f5cb..f17fa7b 100644 --- a/packages/core/src/pages/[locale]/verify-email/page.tsx +++ b/packages/core/src/pages/[locale]/verify-email/page.tsx @@ -1,14 +1,16 @@ 'use client'; import { Suspense, useEffect, useState } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; +import { LocaleLink } from "@/components/locale-link"; +import { useSearchParams } from 'next/navigation'; +import { useRouter as useIntlRouter } from '@/i18n/routing'; import { CheckCircle2, XCircle, Loader2, Mail } from 'lucide-react'; import Link from 'next/link'; import { useLocale, useTranslations } from 'next-intl'; function VerifyEmailPageInner() { const searchParams = useSearchParams(); - const router = useRouter(); + const router = useIntlRouter(); const locale = useLocale(); const t = useTranslations('auth.verifyEmail'); const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); @@ -34,7 +36,7 @@ function VerifyEmailPageInner() { // Redirect to login after 3 seconds setTimeout(() => { - router.push(`/${locale}/login`); + router.push(`/login`); }, 3000); } else { setStatus('error'); @@ -74,12 +76,12 @@ function VerifyEmailPageInner() {

{message}

- {t('goToLogin')} - + )} @@ -94,12 +96,12 @@ function VerifyEmailPageInner() {

{message}

- {t('goToLogin')} - + ) : ( <> @@ -111,18 +113,18 @@ function VerifyEmailPageInner() { {message}

- {t('goToLogin')} - - + {t('requestNew')} - +
)}