From dba82b26a889d8030c11cdb5d5d391720a1756ac Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 8 Oct 2026 08:08:37 +0700 Subject: [PATCH] x --- .../migration.sql | 2 + apps/backend/prisma/schema.prisma | 2 + apps/backend/src/routes/orders.ts | 94 ++++++++++++++++--- .../src/app/[lang]/order/[id]/page.tsx | 45 ++++----- apps/website/src/app/[lang]/track/page.tsx | 21 +++-- apps/website/src/components/ProfileClient.tsx | 27 +++--- .../src/components/TrackOrderClient.tsx | 49 ++++++---- apps/website/src/lib/cms.ts | 5 +- apps/website/src/locales/en/translation.json | 31 ++++++ apps/website/src/locales/it/translation.json | 31 ++++++ apps/website/src/locales/ja/translation.json | 31 ++++++ apps/website/src/locales/ko/translation.json | 31 ++++++ apps/website/src/locales/lo/translation.json | 31 ++++++ apps/website/src/locales/th/translation.json | 31 ++++++ apps/website/src/locales/vi/translation.json | 31 ++++++ apps/website/src/locales/zh/translation.json | 31 ++++++ packages/types/index.ts | 5 + 17 files changed, 427 insertions(+), 71 deletions(-) create mode 100644 apps/backend/prisma/migrations/20261008004733_add_order_item_translations_snapshot/migration.sql diff --git a/apps/backend/prisma/migrations/20261008004733_add_order_item_translations_snapshot/migration.sql b/apps/backend/prisma/migrations/20261008004733_add_order_item_translations_snapshot/migration.sql new file mode 100644 index 0000000..29a7bd9 --- /dev/null +++ b/apps/backend/prisma/migrations/20261008004733_add_order_item_translations_snapshot/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "OrderItem" ADD COLUMN "translations" JSONB; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 4bf6a4c..fe4df13 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -432,8 +432,10 @@ model OrderItem { productName String variantName String? productImage String? + translations Json? } + model Article { id Int @id @default(autoincrement()) slug String @unique diff --git a/apps/backend/src/routes/orders.ts b/apps/backend/src/routes/orders.ts index 8118759..db4e353 100644 --- a/apps/backend/src/routes/orders.ts +++ b/apps/backend/src/routes/orders.ts @@ -15,7 +15,7 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) include: { items: true, customer: true } }); }) - .get("/:id", async ({ params: { id }, set }) => { + .get("/:id", async ({ params: { id }, query, set }) => { const order = await prisma.order.findUnique({ where: { id: Number(id) }, include: { items: true, customer: true } @@ -24,6 +24,42 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) set.status = 404; return { error: "Order not found" }; } + + const lang = (query as any)?.lang; + if (lang && lang !== 'all') { + const missingProductIds = order.items + .filter((item: any) => !item.translations || !(item.translations as any)?.name) + .map((item: any) => item.productId) + .filter(Boolean); + + let legacyTranslationMap = new Map>(); + if (missingProductIds.length > 0) { + const legacyTranslations = await prisma.productTranslation.findMany({ + where: { productId: { in: missingProductIds } }, + select: { productId: true, langCode: true, name: true } + }); + for (const pt of legacyTranslations) { + if (!legacyTranslationMap.has(pt.productId)) legacyTranslationMap.set(pt.productId, {}); + legacyTranslationMap.get(pt.productId)![pt.langCode] = pt.name; + } + } + + const localizedItems = order.items.map((item: any) => { + const snapshotName = (item.translations as any)?.name?.[lang]; + const legacyName = legacyTranslationMap.get(item.productId)?.[lang]; + const localizedName = snapshotName || legacyName || item.productName; + return { + ...item, + productName: localizedName, + }; + }); + + return { + ...order, + items: localizedItems, + }; + } + return order; }) .post("/", async ({ body, set }) => { @@ -31,8 +67,9 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) const data = body as any; const settings = await prisma.storeSettings.findFirst() const earnRate = settings?.pointsEarnRate || 1.0 - const netAmount = data.totalAmount - data.shippingFee - const pointsEarned = Math.floor(Math.max(0, netAmount) * earnRate) + const shippingFee = data.shippingFee || 0 + const netAmount = (data.totalAmount || 0) - shippingFee + const pointsEarned = Number.isFinite(netAmount) ? Math.floor(Math.max(0, netAmount) * earnRate) : 0 if (data.customerId && data.pointsUsed && data.pointsUsed > 0) { // Verify customer has enough points @@ -51,6 +88,33 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) const isPos = data.channel === "POS"; const initialStatus = isPos ? "COMPLETED" : "PENDING_PAYMENT"; + // Pre-fetch product names across all languages for immutable snapshotting + const productIds = Array.from(new Set((data.items || []).map((item: any) => Number(item.productId)).filter(Boolean))) as number[]; + const [productRecords, productTranslations] = await Promise.all([ + productIds.length > 0 ? prisma.product.findMany({ + where: { id: { in: productIds } }, + select: { id: true, name: true } + }) : [], + productIds.length > 0 ? prisma.productTranslation.findMany({ + where: { productId: { in: productIds } }, + select: { productId: true, langCode: true, name: true } + }) : [] + ]); + + const translationMap = new Map>(); + for (const p of productRecords) { + if (p.name) { + if (!translationMap.has(p.id)) translationMap.set(p.id, {}); + translationMap.get(p.id)!['th'] = p.name; + } + } + for (const pt of productTranslations) { + if (pt.name) { + if (!translationMap.has(pt.productId)) translationMap.set(pt.productId, {}); + translationMap.get(pt.productId)![pt.langCode] = pt.name; + } + } + const order = await prisma.order.create({ data: { channel: data.channel || "ONLINE", @@ -77,20 +141,26 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) changeAmount: data.changeAmount, cashierId: data.cashierId, items: { - create: data.items.map((item: any) => ({ - productId: item.productId, - variantId: item.variantId, - quantity: item.quantity, - priceAtPurchase: item.priceAtPurchase, - productName: item.productName, - variantName: item.variantName, - productImage: item.productImage - })) + create: data.items.map((item: any) => { + const names = translationMap.get(Number(item.productId)) || {}; + const translationsSnapshot = Object.keys(names).length > 0 ? { name: names } : undefined; + return { + productId: item.productId, + variantId: item.variantId, + quantity: item.quantity, + priceAtPurchase: item.priceAtPurchase, + productName: item.productName, + variantName: item.variantName, + productImage: item.productImage, + translations: translationsSnapshot, + }; + }) } }, include: { items: true } }); + // For POS orders: Deduct stock immediately and credit points if (isPos) { for (const item of data.items) { diff --git a/apps/website/src/app/[lang]/order/[id]/page.tsx b/apps/website/src/app/[lang]/order/[id]/page.tsx index 92f0fcf..9e854f4 100644 --- a/apps/website/src/app/[lang]/order/[id]/page.tsx +++ b/apps/website/src/app/[lang]/order/[id]/page.tsx @@ -20,7 +20,7 @@ function orderLabel(id: number) { export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise { const { id, lang } = await params const [result, settings] = await Promise.all([ - getOrderResult(id), + getOrderResult(id, lang), getStoreSettings(lang), ]) @@ -45,7 +45,7 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id const { id, lang } = await params const t = getServerTranslations(lang) const [orderResult, categoriesResult, cmsLayoutResult, settings] = await Promise.all([ - getOrderResult(id), + getOrderResult(id, lang), getCategoriesResult(lang), getCmsPageResult('order-success-layout', lang), getStoreSettings(lang), @@ -132,26 +132,29 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id {t.order.items.title}
- {order.items.map((item) => ( -
-
- {item.productName} + {order.items.map((item) => { + const displayName = (item.translations as any)?.name?.[lang] || item.productName + return ( +
+
+ {displayName} +
+
+

{displayName}

+

+ {item.variantName ? `${item.variantName} · ` : ''}x {item.quantity} +

+
+

{formatPrice(item.priceAtPurchase * item.quantity)}

-
-

{item.productName}

-

- {item.variantName ? `${item.variantName} · ` : ''}x {item.quantity} -

-
-

{formatPrice(item.priceAtPurchase * item.quantity)}

-
- ))} + ) + })}
diff --git a/apps/website/src/app/[lang]/track/page.tsx b/apps/website/src/app/[lang]/track/page.tsx index 5492dae..2689493 100644 --- a/apps/website/src/app/[lang]/track/page.tsx +++ b/apps/website/src/app/[lang]/track/page.tsx @@ -5,18 +5,19 @@ import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' import TrackOrderClient from '@/components/TrackOrderClient' import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms' import { splitCmsLayoutSections } from '@/lib/cms-sections' +import { getServerTranslations } from '@/lib/translations' // lang is extracted from params export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) { const { lang } = await params; const settings = await getStoreSettings(lang); + const t = getServerTranslations(lang); const brandName = settings?.brandName || ''; - const description = brandName - ? `ตรวจสอบสถานะคำสั่งซื้อ ${brandName} ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ` - : 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ'; + const baseDesc = t.track?.seoDescription || 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ'; + const description = brandName ? `${baseDesc} - ${brandName}` : baseDesc; return generateSeoMetadata({ - title: 'ติดตามคำสั่งซื้อ', + title: t.track?.seoTitle || 'ติดตามคำสั่งซื้อ', description, path: '/track', lang, @@ -30,6 +31,7 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st getCategoriesResult(lang), getCmsPageResult('track-order-layout', lang), ]) + const t = getServerTranslations(lang) const categories = categoriesResult.ok ? categoriesResult.data : [] const cmsSections = cmsLayoutResult.ok ? cmsLayoutResult.data.sections ?? [] : [] const { announcementSections, heroSections, bottomSections } = splitCmsLayoutSections(cmsSections) @@ -51,16 +53,18 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st
-

Track Order

-

Track Your Status

+

{t.track?.hero?.eyebrow || 'Track Order'}

+

+ {t.track?.hero?.title || 'Track Your Status'} +

- กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้ + {t.track?.hero?.description || 'กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้'}

)}
- +
{bottomSections.length > 0 && ( @@ -69,3 +73,4 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st ) } + diff --git a/apps/website/src/components/ProfileClient.tsx b/apps/website/src/components/ProfileClient.tsx index 1626f20..69dbab2 100644 --- a/apps/website/src/components/ProfileClient.tsx +++ b/apps/website/src/components/ProfileClient.tsx @@ -23,6 +23,7 @@ import { import { formatPrice, type CustomerAddress, type Order } from '@homaroma/types' import { API_BASE } from '../lib/api' +import { useLang } from '../hooks/useLang' import { orderStatusBadgeClass, toneClass } from '../lib/theme' import { useAuthStore, type Customer } from '../store/authStore' @@ -107,6 +108,7 @@ async function readJson(path: string, init?: RequestInit): Promise { } export default function ProfileClient() { + const lang = useLang() const { token, customer, setAuth, logout } = useAuthStore() const [activeTab, setActiveTab] = useState('orders') const [dbCustomer, setDbCustomer] = useState(null) @@ -401,18 +403,21 @@ export default function ProfileClient() {
- {order.items.map((item) => ( -
-
- {item.productName} + {order.items.map((item) => { + const displayName = (item.translations as any)?.name?.[lang] || item.productName + return ( +
+
+ {displayName} +
+
+

{displayName}

+

{item.variantName ? `${item.variantName} · ` : ''}x {item.quantity}

+
+

{formatPrice(item.priceAtPurchase * item.quantity)}

-
-

{item.productName}

-

{item.variantName ? `${item.variantName} · ` : ''}x {item.quantity}

-
-

{formatPrice(item.priceAtPurchase * item.quantity)}

-
- ))} + ) + })}
diff --git a/apps/website/src/components/TrackOrderClient.tsx b/apps/website/src/components/TrackOrderClient.tsx index c36483d..ff6fe00 100644 --- a/apps/website/src/components/TrackOrderClient.tsx +++ b/apps/website/src/components/TrackOrderClient.tsx @@ -2,7 +2,8 @@ import Link from './LocalizedLink' import { useRouter } from 'next/navigation' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { ChevronRight, Loader2, Package, Search } from 'lucide-react' import { API_BASE } from '../lib/api' @@ -31,9 +32,22 @@ function orderLabel(id: number) { return `#${id.toString().padStart(4, '0')}` } -export default function TrackOrderClient() { +interface TrackOrderClientProps { + lang?: string +} + +export default function TrackOrderClient({ lang: propLang }: TrackOrderClientProps = {}) { const router = useRouter() - const lang = useLang() + const contextLang = useLang() + const lang = propLang || contextLang || 'th' + const { t: i18nT } = useTranslation() + const t = useCallback( + (key: string, options?: Record) => { + return i18nT(key, { lng: lang, ...options }) + }, + [i18nT, lang], + ) + const [orderId, setOrderId] = useState('') const [phone, setPhone] = useState('') const [localOrders, setLocalOrders] = useState([]) @@ -61,7 +75,7 @@ export default function TrackOrderClient() { const normalizedOrderId = Number.parseInt(orderId.replace(/[^0-9]/g, ''), 10) if (!Number.isFinite(normalizedOrderId) || normalizedOrderId <= 0) { - setError('กรุณากรอกหมายเลขคำสั่งซื้อให้ถูกต้อง') + setError(t('track.errors.invalidOrderId')) setLoading(false) return } @@ -83,9 +97,9 @@ export default function TrackOrderClient() { return } - setError('error' in data && data.error ? data.error : 'ไม่สามารถยืนยันข้อมูลคำสั่งซื้อได้') + setError('error' in data && data.error ? data.error : t('track.errors.notFound')) } catch (err) { - setError(err instanceof Error ? err.message : 'เกิดข้อผิดพลาดในการเชื่อมต่อ กรุณาลองใหม่อีกครั้ง') + setError(err instanceof Error ? err.message : t('track.errors.networkError')) } finally { setLoading(false) } @@ -99,8 +113,8 @@ export default function TrackOrderClient() {
-

Manual Search

-

ค้นหาคำสั่งซื้อ

+

{t('track.search.eyebrow')}

+

{t('track.search.title')}

@@ -113,7 +127,7 @@ export default function TrackOrderClient() {
setOrderId(event.target.value)} className="ui-form-shell" - placeholder="เช่น 1024" + placeholder={t('track.search.orderIdPlaceholder')} type="text" />
setPhone(event.target.value)} className="ui-form-shell" - placeholder="เบอร์โทรที่ใช้ตอนสั่งซื้อ" + placeholder={t('track.search.phonePlaceholder')} type="tel" />
@@ -146,7 +160,7 @@ export default function TrackOrderClient() { disabled={loading} className="ui-form-inline-button w-full" > - {loading ? : 'Find Order'} + {loading ? : t('track.search.submit')} @@ -154,13 +168,13 @@ export default function TrackOrderClient() {
-

Recent Device Orders

+

{t('track.recent.title')}

{localOrders.length === 0 ? (
-

ยังไม่มีประวัติคำสั่งซื้อบนอุปกรณ์นี้

-

ใช้ฟอร์มค้นหาด้านซ้ายเพื่อบันทึกออเดอร์

+

{t('track.recent.emptyTitle')}

+

{t('track.recent.emptyDesc')}

) : (
@@ -171,7 +185,7 @@ export default function TrackOrderClient() { className="ui-card ui-card-hover group flex items-center justify-between p-5" >
-

Order

+

{t('track.recent.orderLabel')}

{orderLabel(id)}

@@ -185,3 +199,4 @@ export default function TrackOrderClient() {
) } + diff --git a/apps/website/src/lib/cms.ts b/apps/website/src/lib/cms.ts index a8ac668..d12031d 100644 --- a/apps/website/src/lib/cms.ts +++ b/apps/website/src/lib/cms.ts @@ -114,8 +114,9 @@ export async function getActiveCouponsResult(): Promise> { } } -export async function getOrderResult(id: string | number): Promise> { - return await fetchJsonResult(`/orders/${id}`, { next: { revalidate: defaultRevalidate } }) +export async function getOrderResult(id: string | number, lang = 'all'): Promise> { + const query = lang && lang !== 'all' ? `?lang=${lang}` : '' + return await fetchJsonResult(`/orders/${id}${query}`, { next: { revalidate: defaultRevalidate } }) } export async function getProduct(id: string | number, lang = 'all'): Promise { diff --git a/apps/website/src/locales/en/translation.json b/apps/website/src/locales/en/translation.json index 6aa3521..8f8afdf 100644 --- a/apps/website/src/locales/en/translation.json +++ b/apps/website/src/locales/en/translation.json @@ -186,5 +186,36 @@ "loadFailed": "Unable to load this order", "backToTrack": "Back to Order Search" } + }, + "track": { + "seoTitle": "Track Order", + "seoDescription": "Check your order and shipping status using your order number and phone number.", + "hero": { + "eyebrow": "Order Tracking", + "title": "Track Your Status", + "description": "Enter your order number and phone number to check shipment status, or view previously searched orders on this device." + }, + "search": { + "eyebrow": "Manual Search", + "title": "Find Your Order", + "orderIdLabel": "Order Number", + "orderIdPlaceholder": "e.g. 1024", + "phoneLabel": "Phone Number", + "phonePlaceholder": "Phone number used during purchase", + "submit": "Find Order", + "searching": "Searching..." + }, + "recent": { + "title": "Recent Device Orders", + "orderLabel": "Order", + "emptyTitle": "No order history on this device yet", + "emptyDesc": "Use the search form on the left to find and save orders" + }, + "errors": { + "invalidOrderId": "Please enter a valid order number", + "notFound": "Unable to verify order details", + "networkError": "Connection error. Please try again." + } } } + diff --git a/apps/website/src/locales/it/translation.json b/apps/website/src/locales/it/translation.json index 8a92570..1528072 100644 --- a/apps/website/src/locales/it/translation.json +++ b/apps/website/src/locales/it/translation.json @@ -186,5 +186,36 @@ "loadFailed": "Impossibile caricare questo ordine", "backToTrack": "Torna alla ricerca ordine" } + }, + "track": { + "seoTitle": "Traccia Ordine", + "seoDescription": "Verifica lo stato del tuo ordine e della spedizione con il numero d'ordine e il numero di telefono.", + "hero": { + "eyebrow": "Tracciamento Spedizione", + "title": "Traccia il tuo Ordine", + "description": "Inserisci il numero dell'ordine e il numero di telefono per verificare la spedizione o consulta gli ordini cercati su questo dispositivo." + }, + "search": { + "eyebrow": "Ricerca Manuale", + "title": "Trova il tuo Ordine", + "orderIdLabel": "Numero Ordine", + "orderIdPlaceholder": "es. 1024", + "phoneLabel": "Numero di Telefono", + "phonePlaceholder": "Telefono usato durante l'ordine", + "submit": "Cerca Ordine", + "searching": "Ricerca in corso..." + }, + "recent": { + "title": "Ordini Recenti su questo Dispositivo", + "orderLabel": "Ordine", + "emptyTitle": "Nessuna cronologia ordini su questo dispositivo", + "emptyDesc": "Usa il modulo a sinistra per cercare e salvare gli ordini" + }, + "errors": { + "invalidOrderId": "Inserisci un numero d'ordine valido", + "notFound": "Impossibile verificare le informazioni dell'ordine", + "networkError": "Errore di connessione. Riprova più tardi." + } } } + diff --git a/apps/website/src/locales/ja/translation.json b/apps/website/src/locales/ja/translation.json index 3929243..4a0695e 100644 --- a/apps/website/src/locales/ja/translation.json +++ b/apps/website/src/locales/ja/translation.json @@ -207,5 +207,36 @@ "loadFailed": "注文情報を読み込めませんでした", "backToTrack": "注文検索に戻る" } + }, + "track": { + "seoTitle": "注文追跡", + "seoDescription": "注文番号とお電話番号を入力して配送状況をご確認ください。", + "hero": { + "eyebrow": "配送状況の確認", + "title": "ご注文状況を確認する", + "description": "注文番号とお電話番号を入力して配送状況を確認するか、この端末で以前検索した注文を確認できます。" + }, + "search": { + "eyebrow": "手動検索", + "title": "注文を検索", + "orderIdLabel": "注文番号", + "orderIdPlaceholder": "例: 1024", + "phoneLabel": "電話番号", + "phonePlaceholder": "ご注文時の電話番号", + "submit": "注文を検索", + "searching": "検索中..." + }, + "recent": { + "title": "この端末での検索履歴", + "orderLabel": "注文", + "emptyTitle": "この端末での注文履歴はまだありません", + "emptyDesc": "左側の検索フォームから注文を検索・保存してください" + }, + "errors": { + "invalidOrderId": "有効な注文番号を入力してください", + "notFound": "注文情報を確認できませんでした", + "networkError": "接続エラーが発生しました。もう一度お試しください。" + } } } + diff --git a/apps/website/src/locales/ko/translation.json b/apps/website/src/locales/ko/translation.json index c00f35d..60bb0dc 100644 --- a/apps/website/src/locales/ko/translation.json +++ b/apps/website/src/locales/ko/translation.json @@ -207,5 +207,36 @@ "loadFailed": "주문 정보를 불러오지 못했습니다", "backToTrack": "주문 조회로 돌아가기" } + }, + "track": { + "seoTitle": "주문 배송 조회", + "seoDescription": "주문 번호와 연락처로 주문 및 배송 상태를 확인하세요.", + "hero": { + "eyebrow": "주문 조회", + "title": "배송 상태 확인하기", + "description": "주문 번호와 전화번호를 입력하여 배송 상태를 확인하거나 이 기기에서 검색했던 주문을 다시 확인하세요." + }, + "search": { + "eyebrow": "직접 검색", + "title": "주문 검색", + "orderIdLabel": "주문 번호", + "orderIdPlaceholder": "예: 1024", + "phoneLabel": "전화번호", + "phonePlaceholder": "주문 시 입력한 전화번호", + "submit": "주문 찾기", + "searching": "검색 중..." + }, + "recent": { + "title": "이 기기의 최근 조회 주문", + "orderLabel": "주문", + "emptyTitle": "이 기기에 저장된 주문 내역이 없습니다", + "emptyDesc": "왼쪽 검색창에서 주문을 조회하여 저장하세요" + }, + "errors": { + "invalidOrderId": "올바른 주문 번호를 입력해 주세요", + "notFound": "주문 정보를 확인할 수 없습니다", + "networkError": "네트워크 연결 오류가 발생했습니다. 다시 시도해 주세요." + } } } + diff --git a/apps/website/src/locales/lo/translation.json b/apps/website/src/locales/lo/translation.json index 9edc3ec..8c0f20d 100644 --- a/apps/website/src/locales/lo/translation.json +++ b/apps/website/src/locales/lo/translation.json @@ -186,5 +186,36 @@ "loadFailed": "ບໍ່ສາມາດໂຫຼດຂໍ້ມູນຄຳສັ່ງຊື້ໄດ້", "backToTrack": "ກັບໄປຄົ້ນຫາຄຳສັ່ງຊື້" } + }, + "track": { + "seoTitle": "ຕິດຕາມຄຳສັ່ງຊື້", + "seoDescription": "ກວດສອບສະຖານະສິນຄ້າດ້ວຍເລກຄຳສັ່ງຊື້ ແລະ ເບີໂທລະສັບທີ່ໃຊ້ສັ່ງຊື້.", + "hero": { + "eyebrow": "ສະຖານະຄຳສັ່ງຊື້", + "title": "ຕິດຕາມສະຖານະສິນຄ້າຂອງທ່ານ", + "description": "ປ້ອນເລກຄຳສັ່ງຊື້ ແລະ ເບີໂທລະສັບເພື່ອຕິດຕາມສະຖານະສິນຄ້າ ຫຼື ກັບມາເບິ່ງຄຳສັ່ງຊື້ທີ່ເຄີຍຄົ້ນຫາໃນອຸປະກອນນີ້." + }, + "search": { + "eyebrow": "ຄົ້ນຫາດ້ວຍຕົນເອງ", + "title": "ຄົ້ນຫາຄຳສັ່ງຊື້", + "orderIdLabel": "ເລກຄຳສັ່ງຊື້", + "orderIdPlaceholder": "ຕົວຢ່າງ 1024", + "phoneLabel": "ເບີໂທລະສັບ", + "phonePlaceholder": "ເບີໂທລະສັບທີ່ໃຊ້ສັ່ງຊື້", + "submit": "ຄົ້ນຫາຄຳສັ່ງຊື້", + "searching": "ກຳລັງຄົ້ນຫາ..." + }, + "recent": { + "title": "ປະຫວັດຄຳສັ່ງຊື້ໃນອຸປະກອນນີ້", + "orderLabel": "ຄຳສັ່ງຊື້", + "emptyTitle": "ຍັງບໍ່ມີປະຫວັດຄຳສັ່ງຊື້ໃນອຸປະກອນນີ້", + "emptyDesc": "ໃຊ້ແບບຟອມດ້ານຊ້າຍເພື່ອຄົ້ນຫາ ແລະ ບັນທຶກອໍເດີ" + }, + "errors": { + "invalidOrderId": "ກະລຸນາປ້ອນເລກຄຳສັ່ງຊື້ໃຫ້ຖືກຕ້ອງ", + "notFound": "ບໍ່ສາມາດຢືນຢັນຂໍ້ມູນຄຳສັ່ງຊື້ໄດ້", + "networkError": "ເກີດຂໍ້ຜິດພາດໃນການເຊື່ອມຕໍ່ ກະລຸນາລອງໃໝ່ອີກຄັ້ງ" + } } } + diff --git a/apps/website/src/locales/th/translation.json b/apps/website/src/locales/th/translation.json index d6151cf..3662e8c 100644 --- a/apps/website/src/locales/th/translation.json +++ b/apps/website/src/locales/th/translation.json @@ -186,5 +186,36 @@ "loadFailed": "ไม่สามารถโหลดคำสั่งซื้อนี้ได้", "backToTrack": "กลับไปค้นหาคำสั่งซื้อ" } + }, + "track": { + "seoTitle": "ติดตามคำสั่งซื้อ", + "seoDescription": "ตรวจสอบสถานะคำสั่งซื้อด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ", + "hero": { + "eyebrow": "สถานะคำสั่งซื้อ", + "title": "ติดตามสถานะสินค้าของคุณ", + "description": "กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้" + }, + "search": { + "eyebrow": "ค้นหาด้วยตนเอง", + "title": "ค้นหาคำสั่งซื้อ", + "orderIdLabel": "หมายเลขคำสั่งซื้อ", + "orderIdPlaceholder": "เช่น 1024", + "phoneLabel": "เบอร์โทรศัพท์", + "phonePlaceholder": "เบอร์โทรที่ใช้ตอนสั่งซื้อ", + "submit": "ค้นหาคำสั่งซื้อ", + "searching": "กำลังค้นหา..." + }, + "recent": { + "title": "ประวัติคำสั่งซื้อบนอุปกรณ์นี้", + "orderLabel": "คำสั่งซื้อ", + "emptyTitle": "ยังไม่มีประวัติคำสั่งซื้อบนอุปกรณ์นี้", + "emptyDesc": "ใช้ฟอร์มค้นหาด้านซ้ายเพื่อค้นหาและบันทึกออเดอร์" + }, + "errors": { + "invalidOrderId": "กรุณากรอกหมายเลขคำสั่งซื้อให้ถูกต้อง", + "notFound": "ไม่สามารถยืนยันข้อมูลคำสั่งซื้อได้", + "networkError": "เกิดข้อผิดพลาดในการเชื่อมต่อ กรุณาลองใหม่อีกครั้ง" + } } } + diff --git a/apps/website/src/locales/vi/translation.json b/apps/website/src/locales/vi/translation.json index c890cdc..d356662 100644 --- a/apps/website/src/locales/vi/translation.json +++ b/apps/website/src/locales/vi/translation.json @@ -207,5 +207,36 @@ "loadFailed": "Không thể tải thông tin đơn hàng", "backToTrack": "Quay lại tra cứu" } + }, + "track": { + "seoTitle": "Tra cứu đơn hàng", + "seoDescription": "Kiểm tra tình trạng đơn hàng bằng mã đơn hàng và số điện thoại đã đặt.", + "hero": { + "eyebrow": "Tra cứu đơn hàng", + "title": "Theo dõi trạng thái đơn hàng", + "description": "Nhập mã đơn hàng và số điện thoại để kiểm tra trạng thái giao hàng hoặc xem lại các đơn hàng đã tìm kiếm trên thiết bị này." + }, + "search": { + "eyebrow": "Tìm kiếm thủ công", + "title": "Tìm đơn hàng", + "orderIdLabel": "Mã đơn hàng", + "orderIdPlaceholder": "Ví dụ: 1024", + "phoneLabel": "Số điện thoại", + "phonePlaceholder": "Số điện thoại khi đặt hàng", + "submit": "Tìm đơn hàng", + "searching": "Đang tìm kiếm..." + }, + "recent": { + "title": "Đơn hàng gần đây trên thiết bị", + "orderLabel": "Đơn hàng", + "emptyTitle": "Chưa có lịch sử đơn hàng trên thiết bị này", + "emptyDesc": "Sử dụng biểu mẫu bên trái để tìm kiếm và lưu đơn hàng" + }, + "errors": { + "invalidOrderId": "Vui lòng nhập mã đơn hàng hợp lệ", + "notFound": "Không thể xác nhận thông tin đơn hàng", + "networkError": "Lỗi kết nối. Vui lòng thử lại." + } } } + diff --git a/apps/website/src/locales/zh/translation.json b/apps/website/src/locales/zh/translation.json index 6176ef9..3f4017a 100644 --- a/apps/website/src/locales/zh/translation.json +++ b/apps/website/src/locales/zh/translation.json @@ -207,5 +207,36 @@ "loadFailed": "无法加载此订单信息", "backToTrack": "返回订单查询" } + }, + "track": { + "seoTitle": "订单追踪", + "seoDescription": "输入订单编号和手机号码以查询订单和配送状态。", + "hero": { + "eyebrow": "订单追踪", + "title": "追踪您的订单状态", + "description": "输入您的订单编号和电话号码以查看配送状态,或查看此设备上曾查询过的订单。" + }, + "search": { + "eyebrow": "手动查询", + "title": "查询订单", + "orderIdLabel": "订单编号", + "orderIdPlaceholder": "例如 1024", + "phoneLabel": "电话号码", + "phonePlaceholder": "下单时填写的电话号码", + "submit": "查询订单", + "searching": "正在查询..." + }, + "recent": { + "title": "此设备最近查询", + "orderLabel": "订单", + "emptyTitle": "此设备暂无订单记录", + "emptyDesc": "使用左侧搜索框查询并保存订单" + }, + "errors": { + "invalidOrderId": "请输入有效的订单编号", + "notFound": "无法验证订单信息", + "networkError": "网络连接错误,请稍后重试。" + } } } + diff --git a/packages/types/index.ts b/packages/types/index.ts index f00fb33..6d4d8ec 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -485,8 +485,13 @@ export interface OrderItem { productName: string variantName?: string | null productImage?: string | null + translations?: { + name?: Record + variantName?: Record + } | null } + export interface Order { id: number customerName: string