From ee432aac6fe0426c77c5e209f0ad565e126a166b Mon Sep 17 00:00:00 2001 From: tetiana zorii Date: Wed, 28 Jan 2026 14:29:00 -0500 Subject: [PATCH 1/2] (SP:3) feat(i18n): add UA and PL translations for shop/admin pages Add comprehensive i18n support for shop and admin sections in 3 languages (en, uk, pl). Translation coverage: - Shop pages: main page, products, cart, checkout, orders - Admin pages: dashboard, products management, orders management - Navigation: header, mobile menu, category links - Product components: cards, filters, sort, badges (NEW/SALE) - Category names: Apparel, Lifestyle, Collectibles - All UI buttons, labels, and actions Key changes: - Added ~250+ translation keys to messages/en.json, messages/uk.json, messages/pl.json - Updated 20+ components to use useTranslations() and getTranslations() - Implemented color translation in cart and product detail pages - Translated hero message - Added badge translations --- .../shop/admin/orders/[id]/RefundButton.tsx | 10 +- .../app/[locale]/shop/admin/orders/page.tsx | 40 +- frontend/app/[locale]/shop/admin/page.tsx | 14 +- .../app/[locale]/shop/admin/products/page.tsx | 74 ++-- frontend/app/[locale]/shop/cart/page.tsx | 55 +-- .../app/[locale]/shop/checkout/error/page.tsx | 40 +- .../shop/checkout/payment/[orderId]/page.tsx | 61 +-- .../[locale]/shop/checkout/success/page.tsx | 47 +-- .../app/[locale]/shop/orders/[id]/page.tsx | 40 +- frontend/app/[locale]/shop/orders/page.tsx | 69 ++-- frontend/app/[locale]/shop/page.tsx | 27 +- .../[locale]/shop/products/[slug]/page.tsx | 11 +- frontend/app/[locale]/shop/products/page.tsx | 8 +- frontend/components/header/AppMobileMenu.tsx | 26 +- .../components/shared/OnlineCounterPopup.tsx | 12 +- .../components/shop/add-to-cart-button.tsx | 73 ++-- .../shop/admin/admin-pagination.tsx | 20 +- .../admin/admin-product-delete-button.tsx | 22 +- .../admin/admin-product-status-toggle.tsx | 12 +- .../shop/admin/shop-admin-topbar.tsx | 15 +- .../components/shop/catalog-load-more.tsx | 7 +- .../shop/catalog-products-client.tsx | 12 +- frontend/components/shop/category-tile.tsx | 16 +- frontend/components/shop/header/nav-links.tsx | 44 +-- frontend/components/shop/product-card.tsx | 6 +- frontend/components/shop/product-filters.tsx | 24 +- frontend/components/shop/product-sort.tsx | 19 +- frontend/components/shop/products-toolbar.tsx | 14 +- frontend/lib/shop/data.ts | 13 - frontend/messages/en.json | 354 ++++++++++++++++++ frontend/messages/pl.json | 354 ++++++++++++++++++ frontend/messages/uk.json | 354 ++++++++++++++++++ 32 files changed, 1517 insertions(+), 376 deletions(-) diff --git a/frontend/app/[locale]/shop/admin/orders/[id]/RefundButton.tsx b/frontend/app/[locale]/shop/admin/orders/[id]/RefundButton.tsx index b575bd6d..1f00101b 100644 --- a/frontend/app/[locale]/shop/admin/orders/[id]/RefundButton.tsx +++ b/frontend/app/[locale]/shop/admin/orders/[id]/RefundButton.tsx @@ -2,6 +2,7 @@ import { useRouter } from 'next/navigation'; import { useId, useState, useTransition } from 'react'; +import { useTranslations } from 'next-intl'; type Props = { orderId: string; @@ -10,6 +11,7 @@ type Props = { export function RefundButton({ orderId, disabled }: Props) { const router = useRouter(); + const t = useTranslations('shop.admin.refund'); const [isPending, startTransition] = useTransition(); const [error, setError] = useState(null); const errorId = useId(); @@ -59,13 +61,9 @@ export function RefundButton({ orderId, disabled }: Props) { aria-busy={isPending} aria-describedby={error ? errorId : undefined} className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-50" - title={ - disabled - ? 'Refund is only available for paid Stripe orders' - : undefined - } + title={disabled ? t('onlyForPaid') : undefined} > - {isPending ? 'Refunding…' : 'Refund'} + {isPending ? t('refunding') : t('refund')} {error ? ( diff --git a/frontend/app/[locale]/shop/admin/orders/page.tsx b/frontend/app/[locale]/shop/admin/orders/page.tsx index 40efe166..5cf475e6 100644 --- a/frontend/app/[locale]/shop/admin/orders/page.tsx +++ b/frontend/app/[locale]/shop/admin/orders/page.tsx @@ -1,4 +1,5 @@ import { Link } from '@/i18n/routing'; +import { getTranslations } from 'next-intl/server'; import { getAdminOrdersPage } from '@/db/queries/shop/admin-orders'; import { @@ -47,6 +48,7 @@ export default async function AdminOrdersPage({ const { locale } = await params; const sp = await searchParams; + const t = await getTranslations('shop.admin.orders'); const csrfToken = issueCsrfToken('admin:orders:reconcile-stale'); const page = parsePage(sp.page); @@ -74,7 +76,7 @@ export default async function AdminOrdersPage({ itemCount: order.itemCount, paymentProvider: order.paymentProvider ?? '-', viewHref: `/shop/admin/orders/${order.id}`, - viewAriaLabel: `View order ${order.id}`, + viewAriaLabel: t('viewOrder', { id: order.id }), }; }); @@ -91,7 +93,7 @@ export default async function AdminOrdersPage({ id="admin-orders-title" className="text-2xl font-bold text-foreground" > - Admin · Orders + {t('title')}
@@ -100,17 +102,17 @@ export default async function AdminOrdersPage({ type="submit" className="inline-flex w-full items-center justify-center rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary sm:w-auto" > - Reconcile stale + {t('reconcileStale')}
-
+
{/* Mobile cards */}
{viewModels.length === 0 ? (
- No orders yet. + {t('empty')}
) : (
    @@ -138,12 +140,12 @@ export default async function AdminOrdersPage({
    -
    Items
    +
    {t('table.items')}
    {vm.itemCount}
    -
    Provider
    +
    {t('table.provider')}
    -
    Order ID
    +
    {t('table.orderId')}
    - View + {t('actions.view')}
    @@ -182,7 +184,7 @@ export default async function AdminOrdersPage({
    - + @@ -190,43 +192,43 @@ export default async function AdminOrdersPage({ scope="col" className="px-3 py-2 text-left font-semibold text-foreground" > - Created + {t('table.created')} @@ -238,7 +240,7 @@ export default async function AdminOrdersPage({ className="px-3 py-6 text-muted-foreground" colSpan={7} > - No orders yet. + {t('empty')} ) : ( @@ -276,7 +278,7 @@ export default async function AdminOrdersPage({ className="rounded-md border border-border px-2 py-1 text-xs font-medium text-foreground transition-colors hover:bg-secondary" aria-label={vm.viewAriaLabel} > - View + {t('actions.view')} diff --git a/frontend/app/[locale]/shop/admin/page.tsx b/frontend/app/[locale]/shop/admin/page.tsx index e9a771cf..1f980916 100644 --- a/frontend/app/[locale]/shop/admin/page.tsx +++ b/frontend/app/[locale]/shop/admin/page.tsx @@ -1,5 +1,6 @@ // frontend/app/[locale]/shop/admin/page.tsx import { Link } from '@/i18n/routing'; +import { getTranslations } from 'next-intl/server'; import { ShopAdminTopbar } from '@/components/shop/admin/shop-admin-topbar'; import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page'; @@ -8,6 +9,7 @@ export const dynamic = 'force-dynamic'; export default async function ShopAdminHomePage() { await guardShopAdminPage(); + const t = await getTranslations('shop.admin.page'); return ( <> @@ -22,10 +24,10 @@ export default async function ShopAdminHomePage() { id="shop-admin-title" className="text-2xl font-bold text-foreground" > - Shop Admin + {t('title')}

    - Administrative tools for the merch shop. + {t('description')}

    @@ -37,10 +39,10 @@ export default async function ShopAdminHomePage() { className="block rounded-lg border border-border p-4 transition-colors hover:bg-muted/50" >
    - Products + {t('productsSection.title')}
    - Create, edit, activate, feature. + {t('productsSection.description')}
    @@ -51,10 +53,10 @@ export default async function ShopAdminHomePage() { className="block rounded-lg border border-border p-4 transition-colors hover:bg-muted/50" >
    - Orders + {t('ordersSection.title')}
    - Review and manage orders. + {t('ordersSection.description')}
    diff --git a/frontend/app/[locale]/shop/admin/products/page.tsx b/frontend/app/[locale]/shop/admin/products/page.tsx index fe910531..ee1fa46e 100644 --- a/frontend/app/[locale]/shop/admin/products/page.tsx +++ b/frontend/app/[locale]/shop/admin/products/page.tsx @@ -16,6 +16,7 @@ import { } from '@/db/schema'; import { formatMoney, resolveCurrencyFromLocale } from '@/lib/shop/currency'; import { parsePage } from '@/lib/pagination'; +import { getTranslations } from 'next-intl/server'; export const dynamic = 'force-dynamic'; @@ -37,6 +38,7 @@ export default async function AdminProductsPage({ const { locale } = await params; const sp = await searchParams; + const t = await getTranslations('shop.admin.products'); const page = parsePage(sp.page); const offset = (page - 1) * PAGE_SIZE; @@ -102,23 +104,23 @@ export default async function AdminProductsPage({ id="admin-products-title" className="text-2xl font-bold text-foreground" > - Admin · Products + {t('title')} - New product + {t('newProduct')} -
    +
    {/* Mobile cards */}
    {rows.length === 0 ? (
    - No products yet. + {t('empty')}
    ) : (
      @@ -157,7 +159,7 @@ export default async function AdminProductsPage({
      -
      Category
      +
      {t('table.category')}
      -
      Type
      +
      {t('table.type')}
      -
      Stock
      +
      {t('table.stock')}
      {row.stock}
      -
      Badge
      +
      {t('table.badge')}
      {badge}
      -
      Active
      +
      {t('table.active')}
      - {row.isActive ? 'Yes' : 'No'} + {row.isActive ? t('actions.yes') : t('actions.no')}
      -
      Featured
      +
      {t('table.featured')}
      - {row.isFeatured ? 'Yes' : 'No'} + {row.isFeatured ? t('actions.yes') : t('actions.no')}
      -
      Created
      +
      {t('table.created')}
      {formatDate(row.createdAt, locale)}
      @@ -212,17 +214,17 @@ export default async function AdminProductsPage({ - View + {t('actions.view')} - Edit + {t('actions.edit')}
    Orders list{t('listCaption')}
    - Status + {t('table.status')} - Total + {t('table.total')} - Items + {t('table.items')} - Provider + {t('table.provider')} - Order ID + {t('table.orderId')} - Actions + {t('table.actions')}
    - + @@ -258,67 +260,67 @@ export default async function AdminProductsPage({ scope="col" className="w-[20%] px-3 py-2 text-left font-semibold text-foreground" > - Title + {t('table.title')} @@ -371,13 +373,13 @@ export default async function AdminProductsPage({ @@ -390,17 +392,17 @@ export default async function AdminProductsPage({ - View + {t('actions.view')} - Edit + {t('actions.edit')} - No products yet. + {t('empty')} ) : null} diff --git a/frontend/app/[locale]/shop/cart/page.tsx b/frontend/app/[locale]/shop/cart/page.tsx index 53007ac1..5f22926d 100644 --- a/frontend/app/[locale]/shop/cart/page.tsx +++ b/frontend/app/[locale]/shop/cart/page.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import Image from 'next/image'; import { useParams } from 'next/navigation'; import { useRouter } from '@/i18n/routing'; +import { useTranslations } from 'next-intl'; import { Minus, Plus, Trash2, ShoppingBag } from 'lucide-react'; @@ -15,6 +16,8 @@ import { formatMoney } from '@/lib/shop/currency'; export default function CartPage() { const { cart, updateQuantity, removeFromCart } = useCart(); const router = useRouter(); + const t = useTranslations('shop.cart'); + const tColors = useTranslations('shop.catalog.colors'); const [isCheckingOut, setIsCheckingOut] = useState(false); const [checkoutError, setCheckoutError] = useState(null); const [createdOrderId, setCreatedOrderId] = useState(null); @@ -23,6 +26,16 @@ export default function CartPage() { const locale = params.locale ?? 'en'; const shopBase = '/shop'; + const translateColor = (color: string | null | undefined): string | null => { + if (!color) return null; + const colorSlug = color.toLowerCase(); + try { + return tColors(colorSlug); + } catch { + return color; + } + }; + async function handleCheckout() { setCheckoutError(null); setCreatedOrderId(null); @@ -112,16 +125,16 @@ export default function CartPage() { aria-hidden="true" />

    - Your cart is empty + {t('empty')}

    - Looks like you haven't added any items to your cart yet. + {t('emptyDescription')}

    - Start shopping + {t('startShopping')} @@ -131,11 +144,11 @@ export default function CartPage() { return (

    - Your cart + {t('title')}

    -
    +
      {cart.removed.length > 0 && (
    • - Some items were removed from your cart because they are - unavailable or out of stock. + {t('alerts.itemsRemoved')}
    • )} @@ -178,7 +190,7 @@ export default function CartPage() { {(item.selectedSize || item.selectedColor) && (

      - {[item.selectedColor, item.selectedSize] + {[translateColor(item.selectedColor), item.selectedSize] .filter(Boolean) .join(' / ')}

      @@ -195,7 +207,7 @@ export default function CartPage() { ) } className="text-muted-foreground transition-colors hover:text-foreground" - aria-label={`Remove ${item.title} from cart`} + aria-label={t('actions.removeItem', { title: item.title })} >
    @@ -274,12 +286,12 @@ export default function CartPage() { id="order-summary" className="text-lg font-semibold text-foreground" > - Order summary + {t('summary.heading')}
    - Subtotal + {t('summary.subtotal')} {formatMoney( cart.summary.totalAmountMinor, @@ -290,16 +302,16 @@ export default function CartPage() {
    - Shipping + {t('summary.shipping')} - Calculated at checkout + {t('summary.shippingCalc')}
    - Total + {t('summary.total')} {formatMoney( @@ -320,12 +332,11 @@ export default function CartPage() { className="flex w-full items-center justify-center gap-2 rounded-md bg-accent px-6 py-3 text-sm font-semibold uppercase tracking-wide text-accent-foreground transition-colors hover:bg-accent/90 disabled:opacity-60" aria-busy={isCheckingOut} > - {isCheckingOut ? 'Placing order...' : 'Place order'} + {isCheckingOut ? t('checkout.placing') : t('checkout.placeOrder')}

    - You'll either be redirected to secure payment or see - confirmation if payment is not required in this environment. + {t('checkout.message')}

    {/* Fallback CTA if navigation fails after order was created */} @@ -335,7 +346,7 @@ export default function CartPage() { href={`/shop/orders/${encodeURIComponent(createdOrderId)}`} className="text-xs underline underline-offset-4" > - If you are not redirected automatically, open your order + {t('checkout.notRedirected')}
    ) : null} @@ -357,7 +368,7 @@ export default function CartPage() { )}`} className="text-xs underline underline-offset-4" > - Go to order + {t('checkout.goToOrder')}
    ) : null} diff --git a/frontend/app/[locale]/shop/checkout/error/page.tsx b/frontend/app/[locale]/shop/checkout/error/page.tsx index b469c0bc..97b7464b 100644 --- a/frontend/app/[locale]/shop/checkout/error/page.tsx +++ b/frontend/app/[locale]/shop/checkout/error/page.tsx @@ -1,5 +1,6 @@ // frontend/app/[locale]/shop/checkout/error/page.tsx import { Link } from '@/i18n/routing'; +import { getTranslations } from 'next-intl/server'; import { formatMoney, resolveCurrencyFromLocale } from '@/lib/shop/currency'; import { OrderNotFoundError } from '@/lib/services/errors'; @@ -33,6 +34,7 @@ export default async function CheckoutErrorPage({ searchParams?: Promise | SearchParams; }) { const { locale } = await params; + const t = await getTranslations('shop.checkout'); const resolvedSearchParams: SearchParams | undefined = searchParams && typeof (searchParams as any).then === 'function' @@ -52,10 +54,10 @@ export default async function CheckoutErrorPage({ id="checkout-error-title" className="text-2xl font-bold text-foreground" > - Missing order id + {t('errors.missingOrderId')}

    - We couldn’t identify your order. + {t('errors.missingOrderIdDescription')}

    @@ -96,10 +98,10 @@ export default async function CheckoutErrorPage({ id="checkout-error-title" className="text-2xl font-bold text-foreground" > - Order not found + {t('errors.orderNotFound')}

    - We couldn’t find this order. + {t('errors.orderNotFoundDescription')}

    @@ -134,10 +136,10 @@ export default async function CheckoutErrorPage({ id="checkout-error-title" className="text-2xl font-bold text-foreground" > - Unable to load order + {t('errors.unableToLoadOrder')}

    - Please try again later. + {t('errors.tryAgainLater')}

    @@ -165,12 +167,12 @@ export default async function CheckoutErrorPage({ id="checkout-error-title" className="text-3xl font-bold text-foreground" > - {isFailed ? 'Payment failed' : 'Payment status unclear'} + {isFailed ? t('error.paymentFailed') : t('error.paymentUnclear')}

    {isFailed - ? 'The payment for this order was not completed. You can try again or contact support.' - : 'We could not confirm a payment failure for this order.'} + ? t('error.paymentFailedDescription') + : t('error.paymentUnclearDescription')}

    @@ -180,14 +182,14 @@ export default async function CheckoutErrorPage({ >
    -
    Order
    +
    {t('error.orderLabel')}
    {order.id}
    -
    Total
    +
    {t('error.totalLabel')}
    {totalMinor == null ? '-' @@ -196,7 +198,7 @@ export default async function CheckoutErrorPage({
    -
    Status
    +
    {t('error.statusLabel')}
    {order.paymentStatus}
    @@ -209,7 +211,7 @@ export default async function CheckoutErrorPage({ href="/shop/cart" className="inline-flex items-center justify-center rounded-md border border-border px-4 py-2 text-sm font-semibold uppercase tracking-wide text-foreground hover:bg-secondary" > - Back to cart + {t('actions.backToCart')} {isFailed && order.id ? ( @@ -217,7 +219,7 @@ export default async function CheckoutErrorPage({ href={`/shop/checkout/payment/${order.id}`} className="inline-flex items-center justify-center rounded-md bg-accent px-4 py-2 text-sm font-semibold uppercase tracking-wide text-accent-foreground hover:bg-accent/90" > - Retry payment + {t('actions.retryPayment')} ) : null} @@ -225,7 +227,7 @@ export default async function CheckoutErrorPage({ href="/shop/products" className="inline-flex items-center justify-center rounded-md border border-border px-4 py-2 text-sm font-semibold uppercase tracking-wide text-foreground hover:bg-secondary" > - Continue shopping + {t('actions.continueShopping')} diff --git a/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx b/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx index e8c42322..a982b1cf 100644 --- a/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx +++ b/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx @@ -8,6 +8,7 @@ import { orderIdParamSchema } from '@/lib/validation/shop'; import { getStripeEnv } from '@/lib/env/stripe'; import { logError } from '@/lib/logging'; import { ensureStripePaymentIntentForOrder } from '@/lib/services/orders/payment-attempts'; +import { getTranslations } from 'next-intl/server'; export const dynamic = 'force-dynamic'; export const revalidate = 0; @@ -27,16 +28,18 @@ function resolveClientSecret( return raw; } -function buildStatusMessage(status: string) { +async function buildStatusMessage(status: string) { + const t = await getTranslations('shop.checkout.payment.statusMessages'); + if (status === 'paid') { - return 'This order is already paid.'; + return t('alreadyPaid'); } if (status === 'failed') { - return 'The previous payment attempt failed. Please try again.'; + return t('previousFailed'); } - return 'Complete payment to finish your order.'; + return t('completePayment'); } function shouldClearCart( @@ -91,26 +94,28 @@ export default async function PaymentPage(props: PaymentPageProps) { const { locale } = params; const shopBase = `/shop`; + const t = await getTranslations('shop.checkout'); + const orderId = getOrderId(params); if (!orderId) { return ( @@ -125,8 +130,8 @@ export default async function PaymentPage(props: PaymentPageProps) { if (error instanceof OrderNotFoundError) { return ( @@ -151,8 +156,8 @@ export default async function PaymentPage(props: PaymentPageProps) { return ( ); } @@ -193,8 +198,8 @@ export default async function PaymentPage(props: PaymentPageProps) { <> @@ -229,13 +234,13 @@ export default async function PaymentPage(props: PaymentPageProps) {

    - Secure checkout + {t('payment.title')}

    - Pay for order #{order.id.slice(0, 8)} + {t('payment.payForOrder', { orderId: order.id.slice(0, 8) })}

    - {buildStatusMessage(order.paymentStatus)} + {await buildStatusMessage(order.paymentStatus)}

    @@ -248,15 +253,15 @@ export default async function PaymentPage(props: PaymentPageProps) { aria-label="Payment details" >

    - Payment details + {t('payment.paymentDetails')}

    - Complete payment to place your order. + {t('payment.completePayment')}

    - Amount due + {t('payment.amountDue')} {formatMoney(order.totalAmountMinor, order.currency, locale)} @@ -284,22 +289,22 @@ export default async function PaymentPage(props: PaymentPageProps) { aria-label="Order summary" >

    - Order summary + {t('payment.orderSummary')}

    -
    Items
    +
    {t('payment.items')}
    {itemsCount}
    -
    Total amount
    +
    {t('payment.totalAmount')}
    {formatMoney(order.totalAmountMinor, order.currency, locale)}
    -
    Status
    +
    {t('payment.status')}
    {order.paymentStatus}
    diff --git a/frontend/app/[locale]/shop/checkout/success/page.tsx b/frontend/app/[locale]/shop/checkout/success/page.tsx index 44cac05f..dd4672d5 100644 --- a/frontend/app/[locale]/shop/checkout/success/page.tsx +++ b/frontend/app/[locale]/shop/checkout/success/page.tsx @@ -1,5 +1,6 @@ // frontend/app/[locale]/shop/checkout/success/page.tsx import { Link } from '@/i18n/routing'; +import { getTranslations } from 'next-intl/server'; import OrderStatusAutoRefresh from './OrderStatusAutoRefresh'; import { ClearCartOnMount } from '@/components/shop/clear-cart-on-mount'; @@ -77,26 +78,27 @@ export default async function CheckoutSuccessPage({ const { locale } = await params; const resolvedParams = await searchParams; const clearCart = shouldClearCart(resolvedParams); + const t = await getTranslations('shop.checkout'); const orderId = parseOrderId(resolvedParams); if (!orderId) { return ( @@ -112,8 +114,8 @@ export default async function CheckoutSuccessPage({ if (error instanceof OrderNotFoundError) { return ( @@ -138,8 +140,8 @@ export default async function CheckoutSuccessPage({ return ( ); } @@ -159,27 +161,26 @@ export default async function CheckoutSuccessPage({

    - Thank you for your order + {t('success.title')}

    - Order #{order.id.slice(0, 8)} + {t('error.order')} #{order.id.slice(0, 8)}

    - We've received your order. + {t('success.received')} {order.paymentStatus === 'paid' - ? ' Payment has been confirmed.' - : ' Payment is still being processed. This page will update automatically.'} + ? ` ${t('success.paymentConfirmed')}` + : ` ${t('success.paymentProcessing')}`}

    {paymentsDisabled ? (

    - Payments are disabled in this environment. You were not charged for - this order. + {t('success.paymentsDisabled')}

    ) : null} @@ -189,24 +190,24 @@ export default async function CheckoutSuccessPage({ >

    - Order summary + {t('success.orderSummary')}

    -
    Total amount
    +
    {t('success.totalAmount')}
    {formatMoney(totalMinor, order.currency, locale)}
    -
    Items
    +
    {t('success.items')}
    {itemsCount}
    -
    Status
    +
    {t('success.status')}
    {order.paymentStatus}
    @@ -220,13 +221,13 @@ export default async function CheckoutSuccessPage({ href="/shop/products" className="inline-flex items-center justify-center rounded-md bg-accent px-4 py-2 text-sm font-semibold uppercase tracking-wide text-accent-foreground hover:bg-accent/90" > - Continue shopping + {t('success.continueShopping')} - View cart + {t('success.viewCart')}
    diff --git a/frontend/app/[locale]/shop/orders/[id]/page.tsx b/frontend/app/[locale]/shop/orders/[id]/page.tsx index 8849eedc..5265c949 100644 --- a/frontend/app/[locale]/shop/orders/[id]/page.tsx +++ b/frontend/app/[locale]/shop/orders/[id]/page.tsx @@ -4,6 +4,7 @@ import { Link } from '@/i18n/routing'; import { notFound, redirect } from 'next/navigation'; import { unstable_noStore as noStore } from 'next/cache'; import { and, eq } from 'drizzle-orm'; +import { getTranslations } from 'next-intl/server'; import { db } from '@/db'; import { orderItems, orders } from '@/db/schema'; @@ -108,6 +109,7 @@ export default async function OrderDetailPage({ noStore(); const { locale, id } = await params; + const t = await getTranslations('shop.orders.detail'); const user = await getCurrentUser(); if (!user) { @@ -207,7 +209,7 @@ export default async function OrderDetailPage({

    - Order + {t('title')}

    {order.id}
    @@ -220,10 +222,10 @@ export default async function OrderDetailPage({ className="text-sm underline underline-offset-4" href="/shop/orders" > - My orders + {t('myOrders')} - Shop + {t('shop')}
    @@ -233,30 +235,30 @@ export default async function OrderDetailPage({ aria-labelledby="order-summary-heading" >

    - Order summary + {t('orderSummary')}

    -
    Total
    +
    {t('total')}
    {totalFormatted}
    -
    Payment status
    +
    {t('paymentStatus')}
    {String(order.paymentStatus)}
    -
    Created
    +
    {t('created')}
    {createdFormatted}
    {isAdmin && (
    -
    Provider
    +
    {t('provider')}
    {String(order.paymentProvider)}
    )} @@ -265,13 +267,13 @@ export default async function OrderDetailPage({ {isAdmin && (
    -
    Payment reference
    +
    {t('paymentReference')}
    {order.paymentIntentId ?? '—'}
    -
    Idempotency key
    +
    {t('idempotencyKey')}
    {order.idempotencyKey}
    @@ -280,13 +282,13 @@ export default async function OrderDetailPage({ {isAdmin && (
    -
    Stock restored
    +
    {t('stockRestored')}
    {order.stockRestored ? 'true' : 'false'}
    -
    Restocked at
    +
    {t('restockedAt')}
    {restockedFormatted}
    @@ -299,11 +301,11 @@ export default async function OrderDetailPage({ >

    - Items + {t('items')}

    -
      +
        {order.items.map(it => (
      • @@ -316,25 +318,25 @@ export default async function OrderDetailPage({
        {it.productSku - ? `SKU: ${it.productSku}` - : `Product: ${it.productId}`} + ? t('sku', { sku: it.productSku }) + : t('product', { productId: it.productId })}
    -
    Quantity
    +
    {t('quantity')}
    Qty: {it.quantity}
    -
    Unit price
    +
    {t('unitPrice')}
    Unit:{' '} {safeFormatMoneyMajor(it.unitPrice, currency, locale)}
    -
    Line total
    +
    {t('lineTotal')}
    Line:{' '} {safeFormatMoneyMajor(it.lineTotal, currency, locale)} diff --git a/frontend/app/[locale]/shop/orders/page.tsx b/frontend/app/[locale]/shop/orders/page.tsx index 551797b5..5e90fbb3 100644 --- a/frontend/app/[locale]/shop/orders/page.tsx +++ b/frontend/app/[locale]/shop/orders/page.tsx @@ -4,6 +4,7 @@ import { Link } from '@/i18n/routing'; import { redirect } from 'next/navigation'; import { unstable_noStore as noStore } from 'next/cache'; import { desc, eq, sql } from 'drizzle-orm'; +import { getTranslations } from 'next-intl/server'; import { db } from '@/db'; import { orderItems, orders } from '@/db/schema'; @@ -32,25 +33,27 @@ function formatDateTime(d: Date, locale: string) { } } -function statusLabel(status: PaymentStatus) { +function statusLabel( + status: PaymentStatus, + t: Awaited>> +) { switch (status) { case 'paid': - return 'Paid'; + return t('paymentStatus.paid'); case 'pending': - return 'Pending'; + return t('paymentStatus.pending'); case 'requires_payment': - return 'Payment required'; + return t('paymentStatus.requiresPayment'); case 'failed': - return 'Failed'; + return t('paymentStatus.failed'); case 'refunded': - return 'Refunded'; + return t('paymentStatus.refunded'); default: return String(status); } } function statusClassName(status: PaymentStatus) { - // Neutral “nav hover-ish” look as default; only make failures red. switch (status) { case 'failed': return 'border border-border bg-destructive/10 text-destructive'; @@ -80,15 +83,22 @@ function looksLikeUuid(s: string) { ); } -function buildOrderHeadline(primary: string | null, count: number, id: string) { - if (count === 0) return `Order ${shortOrderId(id)} (incomplete)`; +function buildOrderHeadline( + primary: string | null, + count: number, + id: string, + t: Awaited>> +) { + if (count === 0) + return t('orderHeadline.incomplete', { id: shortOrderId(id) }); if (primary) { - if (count > 1) return `${primary} +${count - 1} more`; + if (count > 1) + return t('orderHeadline.withMore', { item: primary, count: count - 1 }); return primary; } - return `Order ${shortOrderId(id)}`; + return t('orderHeadline.default', { id: shortOrderId(id) }); } export default async function MyOrdersPage({ @@ -99,12 +109,11 @@ export default async function MyOrdersPage({ noStore(); const { locale } = await params; + const t = await getTranslations('shop.orders'); const user = await getCurrentUser(); if (!user) { - redirect( - `/login?next=${encodeURIComponent(`/shop/orders`)}` - ); + redirect(`/login?next=${encodeURIComponent(`/shop/orders`)}`); } let rows: Array<{ @@ -114,7 +123,7 @@ export default async function MyOrdersPage({ paymentStatus: PaymentStatus; createdAt: Date; primaryItemLabel: string | null; - itemCount: unknown; // neon може повернути bigint/string + itemCount: unknown; }> = []; try { @@ -126,8 +135,6 @@ export default async function MyOrdersPage({ paymentStatus: orders.paymentStatus, createdAt: orders.createdAt, - // Беремо "перший" non-null label детерміновано (ORDER BY order_items.id), - // без fallback на productId (UUID не повинен ставати назвою). primaryItemLabel: sql` ( array_agg( @@ -174,29 +181,29 @@ export default async function MyOrdersPage({

    - My orders + {t('title')}

    - Your most recent orders (up to 50). + {t('subtitle')}

    {rows.length === 0 ? (
    -

    No orders yet.

    +

    {t('empty.message')}

    - Browse products + {t('empty.browseProducts')}
    @@ -204,7 +211,7 @@ export default async function MyOrdersPage({
    Products list{t('listCaption')}
    - Slug + {t('table.slug')} - Price + {t('table.price')} - Category + {t('table.category')} - Type + {t('table.type')} - Stock + {t('table.stock')} - Badge + {t('table.badge')} - Active + {t('table.active')} - Featured + {t('table.featured')} - Created + {t('table.created')} - Actions + {t('table.actions')}
    - {row.isActive ? 'Yes' : 'No'} + {row.isActive ? t('actions.yes') : t('actions.no')} - {row.isFeatured ? 'Yes' : 'No'} + {row.isFeatured ? t('actions.yes') : t('actions.no')}
    - + @@ -212,25 +219,25 @@ export default async function MyOrdersPage({ scope="col" className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground" > - Items + {t('table.items')} @@ -248,7 +255,7 @@ export default async function MyOrdersPage({ ? rawPrimary : null; - const headline = buildOrderHeadline(primary, count, o.id); + const headline = buildOrderHeadline(primary, count, o.id, t); return ( {headline}
    - Order id: + {t('table.orderId')}: {shortOrderId(o.id)} @@ -285,7 +292,7 @@ export default async function MyOrdersPage({ o.paymentStatus )}`} > - {statusLabel(o.paymentStatus)} + {statusLabel(o.paymentStatus, t)} diff --git a/frontend/app/[locale]/shop/page.tsx b/frontend/app/[locale]/shop/page.tsx index eaaaaf80..c715c1c2 100644 --- a/frontend/app/[locale]/shop/page.tsx +++ b/frontend/app/[locale]/shop/page.tsx @@ -3,6 +3,7 @@ import { ProductCard } from '@/components/shop/product-card'; import { Hero } from '@/components/shop/shop-hero'; import { CategoryTile } from '@/components/shop/category-tile'; import { getHomepageContent } from '@/lib/shop/data'; +import { getTranslations } from 'next-intl/server'; export default async function HomePage({ params, @@ -11,14 +12,15 @@ export default async function HomePage({ }) { const { locale } = await params; const content = await getHomepageContent(locale); + const t = await getTranslations('shop.page'); return ( <>
    - New Arrivals + {t('newArrivals')} - View all + {t('viewAll')} @@ -63,7 +65,7 @@ export default async function HomePage({ id="shop-by-category-heading" className="text-2xl font-bold tracking-tight text-foreground" > - Shop by Category + {t('shopByCategory')} @@ -86,21 +88,20 @@ export default async function HomePage({ id="shop-cta-heading" className="text-2xl font-bold tracking-tight sm:text-3xl" > - Code. Create. Collect. + {t('hero.headline')}

    - Join thousands of developers who express their passion through - premium merch. + {t('hero.subheadline')}

    - Browse all products + {t('hero.cta')}
    diff --git a/frontend/app/[locale]/shop/products/[slug]/page.tsx b/frontend/app/[locale]/shop/products/[slug]/page.tsx index efe19bb1..6f8b7eab 100644 --- a/frontend/app/[locale]/shop/products/[slug]/page.tsx +++ b/frontend/app/[locale]/shop/products/[slug]/page.tsx @@ -3,6 +3,7 @@ import Image from 'next/image'; import { notFound } from 'next/navigation'; import { ArrowLeft } from 'lucide-react'; +import { getTranslations } from 'next-intl/server'; import { AddToCartButton } from '@/components/shop/add-to-cart-button'; import { getProductPageData } from '@/lib/shop/data'; @@ -18,6 +19,8 @@ export default async function ProductPage({ params: Promise<{ slug: string; locale: string }>; }) { const { slug, locale } = await params; + const t = await getTranslations('shop.products'); + const tProduct = await getTranslations('shop.product'); // P0-5 canonical gate: // - slug AND is_active=true @@ -46,7 +49,7 @@ export default async function ProductPage({ className="inline-flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground" >
    List of your orders{t('table.caption')}
    - Date + {t('table.date')} - Status + {t('table.status')} - Total + {t('table.total')}