Files
goodgo-platform/docs/architecture/FILE_MAPPING_GUIDE.md
Ho Ngoc Hai d8b409a9ab
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 18s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m15s
Deploy / Build API Image (push) Failing after 28s
Deploy / Build Web Image (push) Failing after 16s
Deploy / Build AI Services Image (push) Failing after 17s
E2E Tests / Playwright E2E (push) Failing after 31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m46s
Security Scanning / Trivy Scan — Web Image (push) Failing after 1m7s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 53s
Security Scanning / Trivy Filesystem Scan (push) Failing after 35s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 0s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
docs: dịch 22 file Markdown còn lại sang tiếng Việt có dấu (TEC-2881)
Hoàn tất đợt cuối của nhiệm vụ chuyển toàn bộ tài liệu sang tiếng Việt.
Đã dịch 22 file `.md` còn sót (~9.7k dòng) — gồm RUNBOOK, audits,
docs/architecture, docs/load-testing, libs READMEs và các quick references.
Giữ nguyên code blocks, đường dẫn, identifier kỹ thuật, URL và biến môi trường.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 03:26:14 +07:00

18 KiB

GoodGo Frontend: Hướng dẫn triển khai i18n & A11y theo từng file

📋 Ánh xạ file đầy đủ

GIAI ĐOẠN 1: THIẾT LẬP HẠ TẦNG

1. middleware.ts (QUAN TRỌNG)

Hiện tại: Chỉ định tuyến xác thực
Thay đổi:

  • Thêm phát hiện locale từ pathname URL
  • Thêm lưu trữ locale dựa trên cookie
  • Chuyển hướng các đường dẫn /en/*/vi/* một cách phù hợp
  • Thêm dự phòng dựa trên header Accept-Language

Pseudo-code:

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  
  // Extract locale from URL: /en/*, /vi/*, or default
  const locale = extractLocale(pathname) || getPreferredLocale(request);
  
  // ... existing auth logic ...
  
  // If no locale prefix, add it
  if (!locale) {
    return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
  }
  
  // Set locale cookie for client-side
  response.cookies.set('goodgo_locale', locale);
}

2. app/layout.tsx (QUAN TRỌNG)

Hiện tại: Các provider chính, metadata tiếng Việt được hardcode
Thay đổi:

  • Thay đổi lang="vi" thành locale động
  • Cập nhật metadata để hỗ trợ i18n
  • Bao bọc bằng NextIntlClientProvider từ next-intl
  • Giữ nguyên các provider hiện có (ThemeProvider, QueryProvider, AuthProvider)

Các thay đổi chính:

import { getLocale, getTranslations } from 'next-intl/server';

export async function generateMetadata(): Promise<Metadata> {
  const locale = getLocale();
  const t = getTranslations();
  
  return {
    title: t('common.site_name'),
    description: t('common.site_description'),
    openGraph: {
      locale: locale === 'en' ? 'en_US' : 'vi_VN',
      ...
    }
  };
}

3. i18n/config.ts (MỚI)

Tạo file mới với cấu hình i18n:

export const locales = ['en', 'vi'] as const;
export const defaultLocale = 'vi';

export const timeZone = 'Asia/Ho_Chi_Minh';

4. public/locales/en.json (MỚI - FILE LỚN)

Cấu trúc:

{
  "common": {
    "site_name": "GoodGo",
    "site_description": "Smart real estate platform in Vietnam",
    "home": "Home",
    "search": "Search",
    "dashboard": "Dashboard",
    "logout": "Logout",
    "loading": "Loading...",
    "error": "An error occurred",
    "try_again": "Try again"
  },
  "auth": {
    "login": "Login",
    "register": "Register",
    "phone": "Phone number",
    "password": "Password",
    "confirm_password": "Confirm password",
    "no_account": "Don't have an account?",
    "sign_up": "Sign up",
    "sign_in": "Sign in",
    "oauth_failed": "Social login failed. Please try again.",
    "access_denied": "You denied access. Please try again.",
    "invalid_request": "Invalid login request. Please try again.",
    "server_error": "Server error. Please try again later.",
    "show_password": "Show",
    "hide_password": "Hide"
  },
  "property": {
    "apartment": "Apartment",
    "house": "House",
    "villa": "Villa",
    "land": "Land",
    "office": "Office",
    "shophouse": "Shophouse",
    "price": "Price",
    "area": "Area",
    "bedrooms": "Bedrooms",
    "bathrooms": "Bathrooms",
    "direction": "Direction"
  },
  "transaction": {
    "sale": "Sale",
    "rent": "Rent",
    "buy": "Buy",
    "lease": "Lease"
  },
  "direction": {
    "north": "North",
    "south": "South",
    "east": "East",
    "west": "West",
    "northeast": "Northeast",
    "northwest": "Northwest",
    "southeast": "Southeast",
    "southwest": "Southwest"
  },
  "status": {
    "draft": "Draft",
    "pending_review": "Pending review",
    "active": "Active",
    "reserved": "Reserved",
    "sold": "Sold",
    "rented": "Rented",
    "expired": "Expired",
    "rejected": "Rejected"
  },
  "validation": {
    "required": "This field is required",
    "min_length": "Minimum {count} characters",
    "invalid_phone": "Invalid phone number",
    "invalid_email": "Invalid email",
    "passwords_match": "Passwords must match"
  },
  "navigation": {
    "home": "Home",
    "search": "Search",
    "create_listing": "Create listing",
    "my_listings": "My listings",
    "analytics": "Analytics",
    "valuation": "Valuation",
    "profile": "Profile",
    "subscription": "Subscription",
    "payments": "Payments",
    "admin_dashboard": "Admin",
    "admin_users": "Users",
    "admin_kyc": "KYC",
    "admin_moderation": "Moderation"
  },
  "landing": {
    "hero_title": "Find your perfect real estate",
    "hero_subtitle": "Smart real estate platform in Vietnam",
    "search_placeholder": "Enter area, project, or keyword...",
    "featured_listings": "Featured listings",
    "districts": "Popular districts",
    "stats_title": "GoodGo in numbers",
    "stats_listings": "Listings",
    "stats_users": "Users",
    "stats_transactions": "Successful transactions",
    "stats_cities": "Cities",
    "cta_title": "Have a property to list?",
    "cta_subtitle": "List for free today, reach thousands of potential buyers",
    "cta_register": "Sign up free",
    "cta_search": "Search now"
  }
}

5. public/locales/vi.json (MỚI - FILE LỚN)

Cấu trúc giống en.json nhưng với bản dịch tiếng Việt.


GIAI ĐOẠN 2: CẬP NHẬT COMPONENT CỐT LÕI

Các file cần tích hợp Translation Hook

Các file Layout
  1. app/(public)/layout.tsx
// Current: Hardcoded nav items
const navItems = [
  { href: '/', label: 'Trang chủ' },
  { href: '/search', label: 'Tìm kiếm' },
];

// After i18n:
export default function PublicLayout() {
  const t = useTranslations('navigation');
  const navItems = [
    { href: '/', label: t('home') },
    { href: '/search', label: t('search') },
  ];
  
  // Also update footer content
}
  1. app/(dashboard)/layout.tsx
// Update all 8 nav items to use t('navigation.item_name')
// Update theme toggle aria-label
const toggleLabel = theme === 'light' 
  ? t('common.toggle_dark_mode') 
  : t('common.toggle_light_mode');
  1. app/(auth)/layout.tsx Cập nhật mọi thông báo lỗi hoặc nhãn để sử dụng bản dịch.
Các file Page
  1. app/(public)/page.tsx (FILE LỚN) Các phần cần cập nhật:
  • Phần hero (tiêu đề, phụ đề)
  • Form tìm kiếm (placeholder)
  • Các badge loại bất động sản
  • Các khoảng giá
  • Tùy chọn thành phố
  • Tiêu đề các phần
  • Nhãn thống kê
  • Các nút CTA
export default function LandingPage() {
  const t = useTranslations();
  
  const PROPERTY_TYPES_LABELS = PROPERTY_TYPES.map(pt => ({
    value: pt.value,
    label: t(`property.${pt.value.toLowerCase()}`),
  }));
  
  // Update all hardcoded strings:
  // Hero title: t('landing.hero_title')
  // Hero subtitle: t('landing.hero_subtitle')
  // Search placeholder: t('landing.search_placeholder')
  // etc.
}
  1. app/(auth)/login/page.tsx
// Update all form labels to use t()
const phoneLabel = t('auth.phone');
const passwordLabel = t('auth.password');
const loginButton = t('auth.sign_in');

// OAuth error messages - move to translations
const OAUTH_ERROR_MESSAGES = {
  oauth_failed: t('auth.oauth_failed'),
  access_denied: t('auth.access_denied'),
  // ... etc
};
  1. app/(auth)/register/page.tsx Tương tự như trang login.

  2. app/(dashboard)/dashboard/page.tsx và tất cả các trang dashboard khác Cập nhật tiêu đề các phần, trạng thái rỗng, nhãn các nút.

Các trang Search & Listing
  1. app/(public)/search/page.tsx Cập nhật tiêu đề kết quả tìm kiếm, trạng thái rỗng, nhãn bộ lọc.

  2. app/(public)/listings/[id]/page.tsx Cập nhật nhãn chi tiết bất động sản.

  3. app/(dashboard)/listings/page.tsx Cập nhật tiêu đề bảng, nhãn trạng thái, nhãn hành động.

  4. app/(dashboard)/listings/new/page.tsx Sử dụng component listing-form-steps (xem bên dưới).


Các file Component

Các component quan trọng (Làm trước)
  1. components/search/filter-bar.tsx (ƯU TIÊN CAO)
// Current: Hardcoded arrays
const CITIES = ['Hồ Chí Minh', 'Hà Nội', 'Đà Nẵng', ...];
const PRICE_RANGES = [
  { label: 'Dưới 1 tỷ', ... },
  { label: '1 - 3 tỷ', ... },
];

// After i18n:
const CITIES = t('locations.cities').split(',');
const PRICE_RANGES = [
  { label: t('search.price_under_1b'), ... },
  { label: t('search.price_1_3b'), ... },
];
  1. components/listings/listing-form-steps.tsx (ƯU TIÊN CAO - FILE LỚN) Form đa bước này có nhiều nhãn cần dịch:
  • Bước 1: Loại giao dịch, loại bất động sản, tiêu đề, mô tả
  • Bước 2: Các trường địa chỉ, vị trí
  • Bước 3: Diện tích, phòng, phòng tắm, hướng, năm xây dựng, v.v.
  • Bước 4: Giá

Tất cả nhãn các trường nên sử dụng pattern t('form.field_name').

  1. components/auth/oauth-buttons.tsx
// Update button text
<Button>
  {t('auth.google')}  // Currently hardcoded "Google"
</Button>
<Button>
  {t('auth.zalo')}    // Currently hardcoded "Zalo"
</Button>
Các component ưu tiên trung bình
  1. components/search/property-card.tsx
// Update PROPERTY_TYPE_LABELS to use translations
const t = useTranslations();
const PROPERTY_TYPE_LABELS = {
  APARTMENT: t('property.apartment'),
  HOUSE: t('property.house'),
  // ... etc
};

// Update aria-labels to use translations
aria-label={t('property.card_label', { 
  title: listing.property.title,
  type: propertyTypeLabel,
  price: formatPrice(listing.priceVND)
})}
  1. components/listings/listing-status-badge.tsx
// Update status labels
const LISTING_STATUSES = {
  DRAFT: { label: t('status.draft'), variant: 'secondary' },
  ACTIVE: { label: t('status.active'), variant: 'success' },
  // ... etc
};
  1. components/valuation/valuation-form.tsx Cập nhật nhãn form và các nút.

  2. components/listings/image-upload.tsx Cập nhật văn bản nút và thông báo lỗi.

  3. Tất cả các file components/ui/*.tsx có văn bản

  • Button: bất kỳ văn bản mặc định nào
  • Dialog: aria-label nút Đóng
  • Input: thuộc tính placeholder nếu được hardcode
  • Label: bất kỳ văn bản mặc định nào
  • Khác: tương tự

GIAI ĐOẠN 3: VALIDATION & THÔNG BÁO LỖI

1. lib/validations/auth.ts

// Current:
const loginSchema = z.object({
  phone: z.string().min(1, 'Vui lòng nhập số điện thoại'),
  password: z.string().min(1, 'Vui lòng nhập mật khẩu'),
});

// After i18n - move to message files and use in component:
// In component:
const t = useTranslations('validation');
const schema = z.object({
  phone: z.string().min(1, t('required')),
  password: z.string().min(1, t('required')),
});

2. lib/validations/listings.ts (FILE LỚN)

Cập nhật tất cả thông báo lỗi validation Zod:

  • "Vui lòng chọn loại giao dịch" → t('validation.transaction_required')
  • "Tiêu đề tối thiểu 5 ký tự" → t('validation.title_min_length')
  • Tất cả các thông báo validation khác

3. lib/validations/valuation.ts

Mẫu tương tự như listings.


GIAI ĐOẠN 4: CẬP NHẬT TIỆN ÍCH

1. lib/utils.ts

Không thay đổi (đã tối giản).

2. lib/auth-store.ts

// Check if any error messages are hardcoded
// If so, move to i18n and pass locale context

3. lib/api-client.ts

Kiểm tra xem các thông báo lỗi từ API có cần bao bọc i18n hay không.

4. Tất cả các file lib/*-api.ts

Cập nhật xử lý thông báo lỗi nếu cần.


GIAI ĐOẠN 5: CẬP NHẬT KHẢ NĂNG TIẾP CẬN

1. components/ui/dialog.tsx (A11y QUAN TRỌNG)

Thêm quản lý focus:

// Add focus trap
// Save initial focus element
// On mount: move focus to dialog
// On close: restore focus to initial element
// On Escape key: close dialog

import { useEffect, useRef } from 'react';

function Dialog() {
  const initialFocusRef = useRef<HTMLElement>(null);
  
  useEffect(() => {
    // Set initial focus
    const firstButton = dialogRef.current?.querySelector('button');
    firstButton?.focus();
    
    // Trap focus
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Tab') {
        // Prevent focus from leaving dialog
      }
      if (e.key === 'Escape') {
        onClose?.();
      }
    };
    
    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      // Restore focus
      (initialFocusRef.current as HTMLElement | null)?.focus();
    };
  }, []);
}

2. components/ui/input.tsx

Thêm aria-describedby cho thông báo lỗi:

export function Input({ error, ...props }) {
  const errorId = `${props.id}-error`;
  return (
    <>
      <input
        {...props}
        aria-invalid={!!error}
        aria-describedby={error ? errorId : undefined}
      />
      {error && <p id={errorId} role="alert">{error}</p>}
    </>
  );
}

3. components/ui/button.tsx

Đảm bảo tất cả các nút có chỉ báo focus rõ ràng (có thể đã có trong CSS). Thêm aria-busy cho trạng thái loading nếu được sử dụng:

export function Button({ disabled, isLoading, ...props }) {
  return (
    <button
      {...props}
      disabled={disabled || isLoading}
      aria-busy={isLoading}
    >
      {/* content */}
    </button>
  );
}

4. Các component Form

Cập nhật tất cả các form để sử dụng aria-describedby cho thông báo lỗi:

  • app/(auth)/login/page.tsx — Đã có role="alert" ✓ nhưng có thể dùng aria-describedby
  • app/(auth)/register/page.tsx — Tương tự
  • components/listings/listing-form-steps.tsx — Thêm aria-describedby
  • components/search/filter-bar.tsx — Đảm bảo nhãn dễ tiếp cận

5. Tất cả các nút chỉ có biểu tượng

Tìm tất cả các nút chỉ có biểu tượng và thêm aria-label:

// Search in components for: <Button> with only SVG children
// Add aria-label={t('...')}

// Examples:
<Button aria-label={t('common.close')}>
  <X />
</Button>

<Button aria-label={t('common.toggle_dark_mode')}>
  <Moon />
</Button>

6. Loading Spinners

Thêm aria-busy và aria-label:

// In app/(public)/page.tsx and similar:
<div aria-busy={loadingFeatured} aria-label={t('common.loading')}>
  <div className="h-8 w-8 animate-spin rounded-full..." />
</div>

7. components/listings/image-gallery.tsx

Thêm điều hướng bằng bàn phím (phím mũi tên):

// Add keyboard event handler for arrow keys
// Left/Right arrows to navigate images

GIAI ĐOẠN 6: CẬP NHẬT THIẾT LẬP TEST

1. vitest.setup.ts

// Mock next-intl for tests
vi.mock('next-intl', () => ({
  useTranslations: () => (key) => key, // Return key as-is for testing
  getTranslations: async () => (key) => key,
}));

// Or provide full mock messages
const mockMessages = {
  common: { home: 'Home', search: 'Search' },
  auth: { login: 'Login', register: 'Register' },
  // ... etc
};

vi.mock('next-intl', () => ({
  useTranslations: (namespace) => (key) => mockMessages[namespace]?.[key] || key,
}));

2. vitest.config.ts

Có thể cần thêm path alias hoặc thiết lập môi trường test.

3. Cập nhật tất cả các file test trong thư mục __tests__/

  • Thêm prop locale khi render component
  • Test cả tiếng Anh và tiếng Việt nếu có thể
  • Mock các bản dịch i18n

📊 Tóm tắt: Các file theo độ phức tạp cập nhật

Đơn giản (5 phút mỗi file)

  • app/robots.ts
  • app/sitemap.ts
  • components/ui/badge.tsx
  • components/ui/card.tsx
  • components/ui/tabs.tsx

Đơn giản (15-30 phút mỗi file)

  • Các file app/(admin)/*.tsx (3 file)
  • app/(dashboard)/analytics/page.tsx
  • app/(dashboard)/profile/page.tsx
  • app/(dashboard)/subscription/page.tsx
  • app/(dashboard)/payments/page.tsx
  • components/ui/*.tsx (8 file)
  • components/auth/oauth-buttons.tsx
  • components/listings/listing-status-badge.tsx

Trung bình (30-60 phút mỗi file)

  • app/(public)/layout.tsx
  • app/(auth)/login/page.tsx
  • app/(auth)/register/page.tsx
  • app/(dashboard)/layout.tsx
  • app/(dashboard)/dashboard/page.tsx
  • app/(public)/search/page.tsx
  • components/search/property-card.tsx
  • components/search/filter-bar.tsx
  • components/listings/image-upload.tsx
  • components/valuation/*.tsx (3 file)

Phức tạp (1-2 giờ mỗi file)

  • app/(public)/page.tsx (trang landing - nhiều phần)
  • components/listings/listing-form-steps.tsx (form đa bước)
  • components/map/listing-map.tsx (nếu có nhãn)
  • components/charts/*.tsx (3 file - nhãn biểu đồ)

Hạ tầng quan trọng

  • middleware.ts (30-45 phút)
  • app/layout.tsx (30 phút)
  • lib/validations/*.ts (3 file - 45 phút)

Checklist xác minh

Trước khi xem xét i18n + A11y hoàn tất:

Xác minh i18n

  • Cả route /en/*/vi/* đều hoạt động
  • Tất cả văn bản từ file messages, không hardcode
  • Metadata thay đổi theo locale
  • Cookie/header hoạt động cho việc chọn locale
  • Thông báo validation sử dụng i18n
  • Tất cả các enum sử dụng bản dịch
  • Test mock i18n đúng cách

Xác minh A11y

  • Focus trap hoạt động trong dialog
  • Chỉ báo focus hiển thị trên tất cả input
  • Lỗi form được liên kết với aria-describedby
  • Tất cả nút biểu tượng có aria-label
  • Tỷ lệ tương phản màu >= 4.5:1 cho văn bản (chuẩn AA)
  • Điều hướng bàn phím hoạt động ở mọi nơi
  • Test với screen reader (NVDA/JAWS)
  • Loading spinner có aria-busy
  • Tất cả các bảng có header phù hợp

Được tạo: Ngày 9 tháng 4 năm 2026
Độ tin cậy: Cao
Tổng số file ước tính cần cập nhật: 50-60 file