feat(i18n): 添加马来语支持并重构多语言功能
This commit is contained in:
@@ -27,7 +27,7 @@ function GoodsImage({
|
|||||||
const showFallback = !imageUrl || hasError
|
const showFallback = !imageUrl || hasError
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-[116px] w-full overflow-hidden rounded-t-[10px] sm:h-[128px]">
|
<div className="relative w-full overflow-hidden rounded-t-[10px] bg-white/6 aspect-[3/2] min-h-[144px] max-h-[196px] sm:min-h-[156px] sm:max-h-[208px]">
|
||||||
{showFallback ? (
|
{showFallback ? (
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[radial-gradient(circle_at_top,rgba(250,106,0,0.18),transparent_58%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-[12px] text-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[radial-gradient(circle_at_top,rgba(250,106,0,0.18),transparent_58%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-[12px] text-center">
|
||||||
<div className="h-[30px] w-[30px] rounded-[10px] border border-white/10 bg-white/6"></div>
|
<div className="h-[30px] w-[30px] rounded-[10px] border border-white/10 bg-white/6"></div>
|
||||||
@@ -86,10 +86,10 @@ export function GoodsCategoryList({
|
|||||||
{Array.from({length: 4}).map((_, index) => (
|
{Array.from({length: 4}).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${categoryId}-${index}`}
|
key={`${categoryId}-${index}`}
|
||||||
className="liquid-glass-bg flex min-h-[260px] w-full flex-col items-stretch justify-start overflow-hidden"
|
className="liquid-glass-bg flex min-h-[296px] w-full flex-col items-stretch justify-start overflow-hidden sm:min-h-[312px]"
|
||||||
>
|
>
|
||||||
<div className="h-[116px] w-full bg-white/6 sm:h-[128px]"></div>
|
<div className="aspect-[3/2] min-h-[144px] max-h-[196px] w-full bg-white/6 sm:min-h-[156px] sm:max-h-[208px]"></div>
|
||||||
<div className="flex flex-1 flex-col items-start justify-between gap-[12px] p-[12px] sm:p-[14px]">
|
<div className="flex flex-1 flex-col items-start justify-between gap-[14px] p-[14px] sm:p-[16px]">
|
||||||
<div className="w-full space-y-[8px]">
|
<div className="w-full space-y-[8px]">
|
||||||
<div className="h-[18px] w-[72%] rounded-full bg-white/10"></div>
|
<div className="h-[18px] w-[72%] rounded-full bg-white/10"></div>
|
||||||
<div className="h-[14px] w-[48%] rounded-full bg-white/8"></div>
|
<div className="h-[14px] w-[48%] rounded-full bg-white/8"></div>
|
||||||
@@ -148,13 +148,13 @@ export function GoodsCategoryList({
|
|||||||
{category.items.map((product) => (
|
{category.items.map((product) => (
|
||||||
<div
|
<div
|
||||||
key={product.id}
|
key={product.id}
|
||||||
className="liquid-glass-bg pc-hover-float flex min-h-[260px] w-full flex-col items-stretch justify-start overflow-hidden"
|
className="liquid-glass-bg pc-hover-float flex min-h-[296px] w-full flex-col items-stretch justify-start overflow-hidden sm:min-h-[312px]"
|
||||||
>
|
>
|
||||||
<GoodsImage imageUrl={product.imageUrl}/>
|
<GoodsImage imageUrl={product.imageUrl}/>
|
||||||
<div
|
<div
|
||||||
className="flex flex-1 flex-col items-start justify-between gap-[12px] p-[12px] sm:p-[14px]"
|
className="flex flex-1 flex-col items-start justify-between gap-[14px] p-[14px] sm:p-[16px]"
|
||||||
>
|
>
|
||||||
<div className="space-y-[4px]">
|
<div className="w-full space-y-[4px]">
|
||||||
<div className="text-[16px] font-medium text-white">{product.title}</div>
|
<div className="text-[16px] font-medium text-white">{product.title}</div>
|
||||||
<div className="text-[13px] text-white/52">{product.subtitle}</div>
|
<div className="text-[13px] text-white/52">{product.subtitle}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
import i18n from 'i18next'
|
import i18n from 'i18next'
|
||||||
import {initReactI18next} from 'react-i18next'
|
import {initReactI18next} from 'react-i18next'
|
||||||
|
|
||||||
import en from '@/message/en'
|
import en from '@/locale/en'
|
||||||
import zh from '@/message/zh'
|
import ms from '@/locale/ms'
|
||||||
|
import zh from '@/locale/zh'
|
||||||
import {useUserStore} from '@/store/user.ts'
|
import {useUserStore} from '@/store/user.ts'
|
||||||
|
|
||||||
export function normalizeLanguage(language?: string) {
|
export const SUPPORTED_LANGUAGES = ['zh', 'en', 'ms'] as const
|
||||||
|
|
||||||
|
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]
|
||||||
|
|
||||||
|
export function normalizeLanguage(language?: string): AppLanguage {
|
||||||
const value = language?.trim().toLowerCase()
|
const value = language?.trim().toLowerCase()
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return 'zh'
|
return 'zh'
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.startsWith('zh') ? 'zh' : 'en'
|
if (value.startsWith('zh')) {
|
||||||
|
return 'zh'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith('ms')) {
|
||||||
|
return 'ms'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequestLanguageKey(language?: string) {
|
||||||
|
return normalizeLanguage(language).toUpperCase() as Uppercase<AppLanguage>
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialLanguage = normalizeLanguage(useUserStore.getState().language)
|
const initialLanguage = normalizeLanguage(useUserStore.getState().language)
|
||||||
@@ -23,6 +40,7 @@ void i18n
|
|||||||
resources: {
|
resources: {
|
||||||
zh: {translation: zh},
|
zh: {translation: zh},
|
||||||
en: {translation: en},
|
en: {translation: en},
|
||||||
|
ms: {translation: ms},
|
||||||
},
|
},
|
||||||
lng: initialLanguage,
|
lng: initialLanguage,
|
||||||
fallbackLng: 'zh',
|
fallbackLng: 'zh',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ky, {HTTPError, type AfterResponseHook, type Input, type KyInstance, type
|
|||||||
|
|
||||||
import {notifyError, resolveToastMessage} from '@/features/notifications'
|
import {notifyError, resolveToastMessage} from '@/features/notifications'
|
||||||
import i18n from '@/lib/i18n'
|
import i18n from '@/lib/i18n'
|
||||||
import {normalizeLanguage} from '@/lib/i18n'
|
import {getRequestLanguageKey} from '@/lib/i18n'
|
||||||
import {useUserStore} from '@/store/user.ts'
|
import {useUserStore} from '@/store/user.ts'
|
||||||
import type {ValidateTokenData} from '@/types/auth.type.ts'
|
import type {ValidateTokenData} from '@/types/auth.type.ts'
|
||||||
import {objectToFormData} from './tool'
|
import {objectToFormData} from './tool'
|
||||||
@@ -68,7 +68,7 @@ export const setAccessTokenFormatter = (formatter?: TokenFormatter) => {
|
|||||||
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
|
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRequestLanguage = () => normalizeLanguage(useUserStore.getState().language)
|
const getRequestLanguage = () => getRequestLanguageKey(useUserStore.getState().language)
|
||||||
|
|
||||||
const authRefreshClient = ky.create({
|
const authRefreshClient = ky.create({
|
||||||
baseUrl: API_BASE_URL,
|
baseUrl: API_BASE_URL,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const en = {
|
|||||||
language: 'Language',
|
language: 'Language',
|
||||||
chinese: 'Chinese',
|
chinese: 'Chinese',
|
||||||
english: 'English',
|
english: 'English',
|
||||||
|
malay: 'Malay',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
waitingForHostContext: 'Waiting for host context...',
|
waitingForHostContext: 'Waiting for host context...',
|
||||||
@@ -23,8 +24,9 @@ const en = {
|
|||||||
nav: {
|
nav: {
|
||||||
record: 'Record',
|
record: 'Record',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
switchToChinese: '中文',
|
switchToChinese: 'Chinese',
|
||||||
switchToEnglish: 'EN',
|
switchToEnglish: 'English',
|
||||||
|
switchToMalay: 'Malay',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
claimablePoints: 'Claimable Points',
|
claimablePoints: 'Claimable Points',
|
||||||
157
src/locale/ms.ts
Normal file
157
src/locale/ms.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
const ms = {
|
||||||
|
common: {
|
||||||
|
back: 'Kembali',
|
||||||
|
loading: 'Memuatkan...',
|
||||||
|
loadingPage: 'Sedang memuatkan halaman...',
|
||||||
|
processing: 'Sedang diproses...',
|
||||||
|
cancel: 'Batal',
|
||||||
|
confirm: 'Sahkan',
|
||||||
|
close: 'Tutup',
|
||||||
|
noData: 'Tiada data',
|
||||||
|
points: 'Mata',
|
||||||
|
more: 'Lagi',
|
||||||
|
language: 'Bahasa',
|
||||||
|
chinese: 'Bahasa Cina',
|
||||||
|
english: 'Bahasa Inggeris',
|
||||||
|
malay: 'Bahasa Melayu',
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
waitingForHostContext: 'Menunggu konteks hos...',
|
||||||
|
loadingAccountData: 'Sedang memuatkan data akaun...',
|
||||||
|
authenticationFailed: 'Pengesahan gagal',
|
||||||
|
refreshAndTryAgain: 'Sila muat semula dan cuba lagi.',
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
record: 'Rekod',
|
||||||
|
account: 'Akaun',
|
||||||
|
switchToChinese: 'Cina',
|
||||||
|
switchToEnglish: 'Inggeris',
|
||||||
|
switchToMalay: 'Melayu',
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
claimablePoints: 'Mata Boleh Dituntut',
|
||||||
|
claimDescription: 'Kerugian semalam telah ditukar kepada mata. Tuntut untuk digunakan bagi penebusan atau pengeluaran.',
|
||||||
|
dailyClaimLimit: 'Had Tuntutan Harian',
|
||||||
|
claimed: 'Telah Dituntut',
|
||||||
|
availableForWithdrawal: 'Tersedia untuk Pengeluaran (Tunai)',
|
||||||
|
availablePoints: 'Mata Tersedia',
|
||||||
|
cashUnit: 'RM',
|
||||||
|
claimNow: 'Tuntut Sekarang',
|
||||||
|
syncBalance: 'Segerak Baki',
|
||||||
|
syncing: 'Sedang disegerakkan...',
|
||||||
|
confirmClaim: 'Sahkan Tuntutan',
|
||||||
|
confirmClaimDescription: 'Selepas mata yang belum dituntut ditukar menjadi mata yang boleh digunakan, ia boleh digunakan untuk penebusan atau pengeluaran. Adakah anda pasti mahu menuntut?',
|
||||||
|
balanceSyncedSuccessfully: 'Baki berjaya disegerakkan.',
|
||||||
|
claimSubmittedSuccessfully: 'Tuntutan berjaya dihantar.',
|
||||||
|
noClaimablePointsAvailable: 'Tiada mata yang boleh dituntut.',
|
||||||
|
},
|
||||||
|
goods: {
|
||||||
|
categories: {
|
||||||
|
WITHDRAW: 'Pindah ke Platform',
|
||||||
|
BONUS: 'Bonus Permainan',
|
||||||
|
PHYSICAL: 'Hadiah Fizikal',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
WITHDRAW: 'Pindah Sekarang',
|
||||||
|
BONUS: 'Tebus Bonus',
|
||||||
|
PHYSICAL: 'Tuntut Hadiah',
|
||||||
|
},
|
||||||
|
noImage: 'TIADA GAMBAR',
|
||||||
|
loading: 'Memuatkan...',
|
||||||
|
noGoodsAvailableYet: 'Tiada barangan untuk ditebus buat masa ini.',
|
||||||
|
noGoodsForCategory: 'Tiada barangan dalam kategori ini.',
|
||||||
|
confirmWithdrawal: 'Sahkan Pengeluaran',
|
||||||
|
confirmBonusRedemption: 'Sahkan Penebusan Bonus',
|
||||||
|
confirmPhysicalReward: 'Sahkan Hadiah Fizikal',
|
||||||
|
addShippingAddress: 'Tambah Alamat Penghantaran',
|
||||||
|
editShippingAddress: 'Edit Alamat Penghantaran',
|
||||||
|
saveChanges: 'Simpan Perubahan',
|
||||||
|
addressInfo: 'Maklumat Alamat',
|
||||||
|
defaultAddress: 'Alamat Lalai',
|
||||||
|
name: 'Nama',
|
||||||
|
phoneNumber: 'Nombor Telefon',
|
||||||
|
detailedAddress: 'Alamat Terperinci',
|
||||||
|
enterReceiverName: 'Masukkan nama penuh penerima',
|
||||||
|
enterReachablePhone: 'Masukkan nombor telefon yang boleh dihubungi',
|
||||||
|
enterDetailAddress: 'Masukkan alamat terperinci',
|
||||||
|
withdrawalAmount: 'Jumlah Pengeluaran',
|
||||||
|
pointsRequired: 'Mata Diperlukan',
|
||||||
|
turnoverRequirement: 'Syarat Pusingan',
|
||||||
|
submitWithdrawalRequest: 'Hantar permohonan pengeluaran?',
|
||||||
|
item: 'Barangan',
|
||||||
|
pointsCost: 'Kos Mata',
|
||||||
|
pleaseSelectAddressInfo: 'Sila pilih dan isi maklumat alamat.',
|
||||||
|
loadingAddressList: 'Sedang memuatkan senarai alamat...',
|
||||||
|
addAddress: 'Tambah Alamat',
|
||||||
|
selectShippingAddress: 'Sila pilih alamat penghantaran.',
|
||||||
|
addressAddedSuccessfully: 'Alamat berjaya ditambah.',
|
||||||
|
redeemRequestSubmittedSuccessfully: 'Permohonan penebusan berjaya dihantar.',
|
||||||
|
bonusRedeemSubmittedSuccessfully: 'Permohonan penebusan bonus berjaya dihantar.',
|
||||||
|
physicalPrize: 'Hadiah Fizikal',
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
title: 'Akaun',
|
||||||
|
myShippingAddress: 'Alamat Penghantaran Saya',
|
||||||
|
addAddress: 'Tambah Alamat',
|
||||||
|
loadingAddressList: 'Sedang memuatkan senarai alamat...',
|
||||||
|
noShippingAddressFound: 'Tiada alamat penghantaran ditemui. Sila tambah dahulu.',
|
||||||
|
address: 'Alamat',
|
||||||
|
edit: 'Edit',
|
||||||
|
delete: 'Padam',
|
||||||
|
default: 'Lalai',
|
||||||
|
optional: 'Bukan Lalai',
|
||||||
|
addressUpdatedSuccessfully: 'Alamat berjaya dikemas kini.',
|
||||||
|
addressDeletedSuccessfully: 'Alamat berjaya dipadam.',
|
||||||
|
deleteAddress: 'Padam Alamat',
|
||||||
|
deleteAddressFor: 'Padam alamat untuk {{name}}?',
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
title: 'Rekod',
|
||||||
|
myOrders: 'Pesanan Saya',
|
||||||
|
pointsRecord: 'Rekod Mata',
|
||||||
|
loading: 'Memuatkan...',
|
||||||
|
noData: 'Tiada data',
|
||||||
|
checkDetails: 'Lihat butiran',
|
||||||
|
trackingNumber: 'Nombor Penjejakan',
|
||||||
|
orderDetails: 'Butiran Pesanan',
|
||||||
|
orderNumber: 'Nombor Pesanan',
|
||||||
|
orderTime: 'Masa Pesanan',
|
||||||
|
orderType: 'Jenis Pesanan',
|
||||||
|
itemName: 'Nama Barangan',
|
||||||
|
points: 'Mata',
|
||||||
|
status: 'Status',
|
||||||
|
untitledOrder: 'Pesanan Tanpa Tajuk',
|
||||||
|
pointsRecordFallback: 'Rekod Mata',
|
||||||
|
categories: {
|
||||||
|
bonus: 'Bonus Permainan',
|
||||||
|
physical: 'Hadiah Fizikal',
|
||||||
|
withdraw: 'Pindah ke Platform',
|
||||||
|
order: 'Pesanan',
|
||||||
|
},
|
||||||
|
statusLabel: {
|
||||||
|
pending: 'Menunggu',
|
||||||
|
completed: 'Selesai',
|
||||||
|
shipped: 'Dihantar',
|
||||||
|
rejected: 'Ditolak',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
sessionExpired: 'Sesi telah tamat. Sila log masuk semula.',
|
||||||
|
noProductSelected: 'Tiada barangan dipilih.',
|
||||||
|
pleaseSelectShippingAddress: 'Sila pilih alamat penghantaran.',
|
||||||
|
pleaseCompleteAddressFields: 'Sila lengkapkan semua maklumat alamat.',
|
||||||
|
pleaseEnterReceiverName: 'Sila masukkan nama penerima.',
|
||||||
|
pleaseEnterReachablePhone: 'Sila masukkan nombor telefon yang boleh dihubungi.',
|
||||||
|
pleaseEnterDetailedAddress: 'Sila masukkan alamat terperinci.',
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
unauthorized: 'Tidak dibenarkan',
|
||||||
|
requestFailed: 'Permintaan gagal',
|
||||||
|
networkRequestFailed: 'Permintaan rangkaian gagal',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
loading: 'Memuatkan...',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ms
|
||||||
@@ -13,6 +13,7 @@ const zh = {
|
|||||||
language: '语言',
|
language: '语言',
|
||||||
chinese: '中文',
|
chinese: '中文',
|
||||||
english: '英文',
|
english: '英文',
|
||||||
|
malay: '马来文',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
waitingForHostContext: '等待宿主上下文...',
|
waitingForHostContext: '等待宿主上下文...',
|
||||||
@@ -24,7 +25,8 @@ const zh = {
|
|||||||
record: '记录',
|
record: '记录',
|
||||||
account: '我的',
|
account: '我的',
|
||||||
switchToChinese: '中文',
|
switchToChinese: '中文',
|
||||||
switchToEnglish: 'EN',
|
switchToEnglish: '英文',
|
||||||
|
switchToMalay: '马来文',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
claimablePoints: '待领取积分',
|
claimablePoints: '待领取积分',
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
|
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
import {Languages} from 'lucide-react'
|
import {Check, Languages} from 'lucide-react'
|
||||||
import {useTranslation} from 'react-i18next'
|
import {useTranslation} from 'react-i18next'
|
||||||
|
|
||||||
import PageLayout from '@/components/layout'
|
import PageLayout from '@/components/layout'
|
||||||
@@ -33,7 +33,13 @@ import {claim} from '@/api/business.ts'
|
|||||||
import {notifyError, notifySuccess} from '@/features/notifications'
|
import {notifyError, notifySuccess} from '@/features/notifications'
|
||||||
import {MotionButton, MotionLink, tapMotionProps} from '@/lib/motion'
|
import {MotionButton, MotionLink, tapMotionProps} from '@/lib/motion'
|
||||||
import {useUserStore} from "@/store/user.ts";
|
import {useUserStore} from "@/store/user.ts";
|
||||||
import {normalizeLanguage} from '@/lib/i18n'
|
import {type AppLanguage, normalizeLanguage} from '@/lib/i18n'
|
||||||
|
|
||||||
|
const LANGUAGE_OPTIONS: Array<{value: AppLanguage; labelKey: 'switchToChinese' | 'switchToEnglish' | 'switchToMalay'}> = [
|
||||||
|
{value: 'zh', labelKey: 'switchToChinese'},
|
||||||
|
{value: 'en', labelKey: 'switchToEnglish'},
|
||||||
|
{value: 'ms', labelKey: 'switchToMalay'},
|
||||||
|
]
|
||||||
|
|
||||||
function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
|
function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
|
||||||
return (
|
return (
|
||||||
@@ -65,6 +71,7 @@ function getProgressPercent(current = 0, total = 0) {
|
|||||||
function HomePage() {
|
function HomePage() {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
||||||
|
const [languageModalOpen, setLanguageModalOpen] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const authInfo = useUserStore(state => state.authInfo)
|
const authInfo = useUserStore(state => state.authInfo)
|
||||||
const language = useUserStore((state) => state.language)
|
const language = useUserStore((state) => state.language)
|
||||||
@@ -87,6 +94,7 @@ function HomePage() {
|
|||||||
const {assetsInfo} = useAssetsQuery()
|
const {assetsInfo} = useAssetsQuery()
|
||||||
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
|
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
|
||||||
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
|
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
|
||||||
|
const currentLanguage = normalizeLanguage(language)
|
||||||
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
|
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
|
||||||
...category,
|
...category,
|
||||||
items: category.items.slice(0, 4),
|
items: category.items.slice(0, 4),
|
||||||
@@ -136,9 +144,9 @@ function HomePage() {
|
|||||||
navigate(`/goods?type=${type}`)
|
navigate(`/goods?type=${type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleLanguage = () => {
|
const handleSelectLanguage = (nextLanguage: AppLanguage) => {
|
||||||
const currentLanguage = normalizeLanguage(language)
|
setLanguage(nextLanguage)
|
||||||
setLanguage(currentLanguage === 'zh' ? 'en' : 'zh')
|
setLanguageModalOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -150,7 +158,7 @@ function HomePage() {
|
|||||||
<MotionButton
|
<MotionButton
|
||||||
type="button"
|
type="button"
|
||||||
className="liquid-glass-bg flex items-center justify-between gap-[12px] rounded-[12px] px-[12px] py-[8px] text-[13px] text-white/88 transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
className="liquid-glass-bg flex items-center justify-between gap-[12px] rounded-[12px] px-[12px] py-[8px] text-[13px] text-white/88 transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
onClick={handleToggleLanguage}
|
onClick={() => setLanguageModalOpen(true)}
|
||||||
{...tapMotionProps}
|
{...tapMotionProps}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-[10px]">
|
<div className="flex min-w-0 items-center gap-[10px]">
|
||||||
@@ -159,7 +167,7 @@ function HomePage() {
|
|||||||
<Languages className="h-[16px] w-[16px] shrink-0" aria-hidden="true"/>
|
<Languages className="h-[16px] w-[16px] shrink-0" aria-hidden="true"/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="truncate font-medium">{normalizeLanguage(language) === 'zh' ? t('nav.switchToEnglish') : t('nav.switchToChinese')}</div>
|
className="truncate font-medium">{t(`nav.${LANGUAGE_OPTIONS.find((option) => option.value === currentLanguage)?.labelKey ?? 'switchToChinese'}`)}</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="h-[16px] w-[16px] shrink-0 text-white/70" aria-hidden="true"/>
|
<ChevronRight className="h-[16px] w-[16px] shrink-0 text-white/70" aria-hidden="true"/>
|
||||||
</MotionButton>
|
</MotionButton>
|
||||||
@@ -303,6 +311,27 @@ function HomePage() {
|
|||||||
onChangeAddressForm={redeem.changeAddressForm}
|
onChangeAddressForm={redeem.changeAddressForm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={languageModalOpen}
|
||||||
|
title={t('common.language')}
|
||||||
|
onClose={() => setLanguageModalOpen(false)}
|
||||||
|
className="max-w-[420px]"
|
||||||
|
bodyClassName="space-y-3"
|
||||||
|
>
|
||||||
|
{LANGUAGE_OPTIONS.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
variant={option.value === currentLanguage ? 'orange' : 'gray'}
|
||||||
|
className="flex h-[46px] w-full items-center justify-between px-[14px] text-[14px]"
|
||||||
|
onClick={() => handleSelectLanguage(option.value)}
|
||||||
|
>
|
||||||
|
<span>{t(`nav.${option.labelKey}`)}</span>
|
||||||
|
{option.value === currentLanguage ? <Check className="h-[16px] w-[16px] shrink-0"/> : null}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={claimModalOpen}
|
open={claimModalOpen}
|
||||||
title={t('home.confirmClaim')}
|
title={t('home.confirmClaim')}
|
||||||
|
|||||||
Reference in New Issue
Block a user