diff --git a/src/app/(home)/layout.tsx b/src/app/(home)/layout.tsx
index 02dc6bfe..3f39b422 100644
--- a/src/app/(home)/layout.tsx
+++ b/src/app/(home)/layout.tsx
@@ -1,5 +1,6 @@
import { ConnectButton } from "~/components/common/ConnectButton"
import { DarkModeSwitch } from "~/components/common/DarkModeSwitch"
+import { LanguageSwitch } from "~/components/common/LanguageSwitch"
import { Logo } from "~/components/common/Logo"
import HomeTabs from "~/components/home/HomeTabs"
import PromotionLinks from "~/components/home/PromotionLinks"
@@ -45,6 +46,7 @@ export default async function HomeLayout({
+
©{" "}
+ }
+ dropdown={
+ <>
+ {languages.map((language, i) => (
+ {
+ changeLanguage(language.code)
+ }}
+ className="mx-auto"
+ >
+ {language.name}
+ {language.active && (
+
+ )}
+
+ ))}
+ >
+ }
+ />
+ )
+}
diff --git a/src/components/site/SiteFooter.tsx b/src/components/site/SiteFooter.tsx
index 1f863869..97ed50c0 100644
--- a/src/components/site/SiteFooter.tsx
+++ b/src/components/site/SiteFooter.tsx
@@ -6,6 +6,7 @@ import { Trans, getTranslation } from "~/lib/i18n"
import { ExpandedCharacter } from "~/lib/types"
import { DarkModeSwitch } from "../common/DarkModeSwitch"
+import { LanguageSwitch } from "../common/LanguageSwitch"
import { UniLink } from "../ui/UniLink"
import ConnectedAccounts from "./ConnectedAccounts"
@@ -52,7 +53,10 @@ export default async function SiteFooter({
-
+
+
+
+
{site?.metadata?.content?.ga && (
diff --git a/src/components/ui/Menu.tsx b/src/components/ui/Menu.tsx
index 27ae2a98..caf7dd2c 100644
--- a/src/components/ui/Menu.tsx
+++ b/src/components/ui/Menu.tsx
@@ -1,7 +1,7 @@
import Link from "next/link"
import React, { Fragment } from "react"
-import { autoUpdate, shift, useFloating } from "@floating-ui/react"
+import { Placement, autoUpdate, shift, useFloating } from "@floating-ui/react"
import { Menu as HeadlessUiMenu } from "@headlessui/react"
import { cn } from "~/lib/utils"
@@ -19,7 +19,7 @@ export function Menu({
}: React.PropsWithChildren<{
target: JSX.Element
dropdown: React.ReactNode
- placement?: "bottom-start" | "bottom-end"
+ placement?: Placement
}>) {
const { refs, floatingStyles } = useFloating({
placement,
diff --git a/src/lib/accept-lang.ts b/src/lib/accept-lang.ts
index 5fc14a07..a12b884d 100644
--- a/src/lib/accept-lang.ts
+++ b/src/lib/accept-lang.ts
@@ -1,19 +1,25 @@
-import { headers } from "next/headers"
+import { cookies, headers } from "next/headers"
import { fallbackLng } from "~/lib/i18n/settings"
export function getAcceptLang() {
let lng = fallbackLng
+ const preferLang = cookies().get("preferred_language")?.value
+
let acceptLang = headers().get("accept-language")?.split(",")[0]
if (acceptLang === "zh-HK" || acceptLang === "zh-TW") {
acceptLang = "zh-TW"
} else {
acceptLang = acceptLang?.split("-")[0]
}
+
if (acceptLang) {
lng = acceptLang
}
+ if (preferLang) {
+ lng = preferLang
+ }
return lng
}
diff --git a/src/lib/i18n/client.ts b/src/lib/i18n/client.ts
index 70cbba83..3b644720 100644
--- a/src/lib/i18n/client.ts
+++ b/src/lib/i18n/client.ts
@@ -2,6 +2,7 @@
import i18next from "i18next"
import resourcesToBackend from "i18next-resources-to-backend"
+import { useEffect, useState } from "react"
import {
initReactI18next,
useTranslation as useTranslationOrg,
@@ -10,7 +11,7 @@ import { Trans as TransW } from "react-i18next/TransWithoutContext"
import { useLang } from "~/hooks/useLang"
-import { defaultNS, getOptions, languages } from "./settings"
+import { defaultNS, getOptions, languageNames, languages } from "./settings"
i18next
.use(initReactI18next)
@@ -33,3 +34,46 @@ export function useTranslation(ns: string = defaultNS) {
}
export const Trans = TransW
+
+export function isLanguageAuto() {
+ return !window.document.cookie.includes("preferred_language")
+}
+
+export function changeLanguage(language: string) {
+ if (language === "auto") {
+ document.cookie =
+ "preferred_language=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"
+ } else {
+ document.cookie = `preferred_language=${language};`
+ }
+ window.location.reload()
+}
+
+export function useAvailableLanguages() {
+ const supportedLngs = i18next.options.supportedLngs
+ const languages =
+ typeof supportedLngs === "object"
+ ? supportedLngs.filter((i) => i !== "cimode")
+ : []
+ const [isMounted, setIsMounted] = useState(false)
+
+ const [currentLng, setCurrentLng] = useState(i18next.language)
+
+ useEffect(() => {
+ if (!isMounted) {
+ setIsMounted(true)
+ return
+ }
+ if (isMounted && isLanguageAuto()) {
+ setCurrentLng("auto")
+ }
+ }, [isMounted])
+
+ return languages.concat("auto").map((lang) => {
+ return {
+ code: lang,
+ name: languageNames[lang as keyof typeof languageNames],
+ active: lang === currentLng,
+ }
+ })
+}
diff --git a/src/lib/i18n/settings.ts b/src/lib/i18n/settings.ts
index 4da7f705..7b70c27a 100644
--- a/src/lib/i18n/settings.ts
+++ b/src/lib/i18n/settings.ts
@@ -1,5 +1,12 @@
export const fallbackLng = "en"
export const languages = ["en", "zh", "zh-TW", "ja"]
+export const languageNames = {
+ en: "English",
+ zh: "中文",
+ "zh-TW": "中文(繁體)",
+ ja: "日本語",
+ auto: "Auto",
+}
export const defaultNS = "common"
export function getOptions(lng = fallbackLng, ns = defaultNS) {