From 36c1e3b39a7346e5f428e9fab66bd0a86e625547 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 8 Apr 2026 13:21:37 +0700 Subject: [PATCH] fix(web): add proper Vietnamese diacritics to all dashboard and listing pages Vietnamese text throughout the frontend was missing accent marks (diacritics), using plain ASCII instead of proper Unicode characters. Fixed all user-visible text across dashboard, analytics, listings, search, and chart components. Co-Authored-By: Paperclip --- apps/web/app/(dashboard)/analytics/page.tsx | 100 +++++++++--------- apps/web/app/(dashboard)/dashboard/page.tsx | 72 ++++++------- apps/web/app/(dashboard)/listings/page.tsx | 62 +++++------ apps/web/app/(public)/listings/[id]/page.tsx | 70 ++++++------ apps/web/app/(public)/search/page.tsx | 2 +- .../components/charts/district-bar-chart.tsx | 4 +- .../components/charts/price-trend-chart.tsx | 6 +- 7 files changed, 158 insertions(+), 158 deletions(-) diff --git a/apps/web/app/(dashboard)/analytics/page.tsx b/apps/web/app/(dashboard)/analytics/page.tsx index 8e2436c..9012094 100644 --- a/apps/web/app/(dashboard)/analytics/page.tsx +++ b/apps/web/app/(dashboard)/analytics/page.tsx @@ -15,12 +15,12 @@ import { const DistrictBarChart = dynamic( () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), - { ssr: false, loading: () =>
Dang tai bieu do...
}, + { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const PriceTrendChart = dynamic( () => import('@/components/charts/price-trend-chart').then((mod) => mod.PriceTrendChart), - { ssr: false, loading: () =>
Dang tai bieu do...
}, + { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const CITIES = ['Ho Chi Minh', 'Ha Noi', 'Da Nang']; @@ -29,14 +29,14 @@ const TREND_PERIODS = ['2025-Q1', '2025-Q2', '2025-Q3', '2025-Q4', '2026-Q1']; function formatPrice(priceStr: string): string { const num = Number(priceStr); - if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} ty`; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`; + if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`; + if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`; return num.toLocaleString('vi-VN'); } function formatPriceM2(price: number): string { - if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m2`; - return `${price.toLocaleString('vi-VN')} d/m2`; + if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`; + return `${price.toLocaleString('vi-VN')} đ/m²`; } function YoYBadge({ value }: { value: number | null }) { @@ -91,7 +91,7 @@ export default function AnalyticsPage() { setTrendDistrict(firstDistrict); } }) - .catch(() => setError('Khong the tai du lieu phan tich')) + .catch(() => setError('Không thể tải dữ liệu phân tích')) .finally(() => setLoading(false)); }, [city, period]); @@ -131,16 +131,16 @@ export default function AnalyticsPage() { const trendChartData = priceTrend.map((p) => ({ period: p.period, 'Gia/m2': Math.round(p.avgPriceM2 / 1_000_000), - 'Tin dang': p.totalListings, + 'Tin đăng': p.totalListings, })); return (
-

Phan tich thi truong

+

Phân tích thị trường

- Bao cao thi truong bat dong san - {period} + Báo cáo thị trường bất động sản - {period}

@@ -163,7 +163,7 @@ export default function AnalyticsPage() {
- Tong tin dang + Tổng tin đăng {loading ? '...' : totalListings.toLocaleString('vi-VN')} @@ -171,7 +171,7 @@ export default function AnalyticsPage() { - Gia TB/m2 + Giá TB/m² {loading ? '...' : formatPriceM2(avgPriceM2)} @@ -179,15 +179,15 @@ export default function AnalyticsPage() { - Ngay trung binh de ban + Ngày trung bình để bán - {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngay`} + {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngày`} - So quan/huyen + Số quận/huyện {loading ? '...' : new Set(marketReport.map((d) => d.district)).size} @@ -198,9 +198,9 @@ export default function AnalyticsPage() { {/* Tabs */} - Tong quan - Xu huong gia - Chi tiet quan + Tổng quan + Xu hướng giá + Chi tiết quận {/* Overview Tab */} @@ -209,17 +209,17 @@ export default function AnalyticsPage() { {/* Bar Chart - Price by District */} - Gia trung binh theo quan - Trieu VND/m2 tai {city} + Giá trung bình theo quận + Triệu VND/m² tại {city} {loading ? (
- Dang tai... + Đang tải...
) : barChartData.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : ( @@ -230,17 +230,17 @@ export default function AnalyticsPage() { {/* Heatmap - Card Grid */} - Ban do gia theo quan - So sanh gia trung binh/m2 tai {city} + Bản đồ giá theo quận + So sánh giá trung bình/m² tại {city} {loading ? (
- Dang tai... + Đang tải...
) : heatmap.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : (
@@ -267,7 +267,7 @@ export default function AnalyticsPage() { {formatPriceM2(point.avgPriceM2)}
- {point.totalListings} tin dang + {point.totalListings} tin đăng
); @@ -299,20 +299,20 @@ export default function AnalyticsPage() { - Xu huong gia - {trendDistrict || 'Chon quan'} + Xu hướng giá - {trendDistrict || 'Chọn quận'} - Bien dong gia trung binh/m2 qua cac quy (Can ho) + Biến động giá trung bình/m² qua các quý (Căn hộ) {trendLoading ? (
- Dang tai... + Đang tải...
) : trendChartData.length === 0 ? (
- Chua co du lieu xu huong + Chưa có dữ liệu xu hướng
) : ( @@ -328,31 +328,31 @@ export default function AnalyticsPage() { {/* Stats Table */} - Thong ke chi tiet theo quan + Thống kê chi tiết theo quận - Du lieu thi truong bat dong san tai {city} - {period} + Dữ liệu thị trường bất động sản tại {city} - {period} {loading ? (
- Dang tai... + Đang tải...
) : districtStats.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : (
- - - - - - + + + + + + @@ -391,17 +391,17 @@ export default function AnalyticsPage() { {/* Market Report Cards */} - Bao cao thi truong - Tong hop chi so thi truong theo tung quan + Báo cáo thị trường + Tổng hợp chỉ số thị trường theo từng quận {loading ? (
- Dang tai... + Đang tải...
) : marketReport.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : (
@@ -411,25 +411,25 @@ export default function AnalyticsPage() {

{district.district}

- Gia trung vi + Giá trung vị {formatPrice(district.medianPrice)}
- Gia/m2 + Giá/m² {formatPriceM2(district.avgPriceM2)}
- Tin dang + Tin đăng {district.totalListings}
- Ton kho + Tồn kho {district.inventoryLevel}
- Thay doi YoY + Thay đổi YoY
diff --git a/apps/web/app/(dashboard)/dashboard/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx index ef80e3d..07e40f8 100644 --- a/apps/web/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/page.tsx @@ -16,7 +16,7 @@ import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/lis const DistrictBarChart = dynamic( () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), - { ssr: false, loading: () =>
Dang tai bieu do...
}, + { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const CITY = 'Ho Chi Minh'; @@ -24,14 +24,14 @@ const PERIOD = '2026-Q1'; function formatPrice(priceStr: string): string { const num = Number(priceStr); - if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} ty`; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`; + if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`; + if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`; return num.toLocaleString('vi-VN'); } function formatPriceM2(price: number): string { - if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m2`; - return `${price.toLocaleString('vi-VN')} d/m2`; + if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`; + return `${price.toLocaleString('vi-VN')} đ/m²`; } interface StatCardProps { @@ -124,35 +124,35 @@ export default function DashboardPage() {
-

Bang dieu khien

+

Bảng điều khiển

- Tong quan thi truong va tin dang cua ban + Tổng quan thị trường và tin đăng của bạn

- +
{/* Stats overview */}
- Gia trung binh theo quan - {CITY} - {PERIOD} (trieu VND/m2) + Giá trung bình theo quận + {CITY} - {PERIOD} (triệu VND/m²) {loading ? (
- Dang tai... + Đang tải...
) : chartData.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : ( [`${value} tr/m2`, 'Gia']} + tooltipFormatter={(value) => [`${value} tr/m²`, 'Giá']} /> )}
@@ -190,30 +190,30 @@ export default function DashboardPage() { {/* Market summary */} - Thi truong {CITY} - Chi so chinh - {PERIOD} + Thị trường {CITY} + Chỉ số chính - {PERIOD}
- Tong tin dang + Tổng tin đăng {loading ? '...' : totalListings.toLocaleString('vi-VN')}
- Gia TB/m2 + Giá TB/m² {loading ? '...' : formatPriceM2(avgPriceM2)}
- Ngay TB de ban + Ngày TB để bán - {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngay`} + {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngày`}
- So quan + Số quận {loading ? '...' : new Set(marketReport.map((d) => d.district)).size} @@ -221,7 +221,7 @@ export default function DashboardPage() {
@@ -233,26 +233,26 @@ export default function DashboardPage() {
- Tin dang gan day - Danh sach tin dang moi nhat cua ban + Tin đăng gần đây + Danh sách tin đăng mới nhất của bạn
{loading ? (
- Dang tai... + Đang tải...
) : !listings || listings.data.length === 0 ? (
-

Chua co tin dang nao

+

Chưa có tin đăng nào

@@ -292,8 +292,8 @@ export default function DashboardPage() {
- {listing.viewCount} luot xem - {listing.inquiryCount} lien he + {listing.viewCount} lượt xem + {listing.inquiryCount} liên hệ
))} diff --git a/apps/web/app/(dashboard)/listings/page.tsx b/apps/web/app/(dashboard)/listings/page.tsx index 4a46433..9f66c1b 100644 --- a/apps/web/app/(dashboard)/listings/page.tsx +++ b/apps/web/app/(dashboard)/listings/page.tsx @@ -16,8 +16,8 @@ import { import { PROPERTY_TYPES, TRANSACTION_TYPES, LISTING_STATUSES } from '@/lib/validations/listings'; function formatPrice(priceVND: string): string { const num = Number(priceVND); - if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} ty`; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`; + if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`; + if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`; return num.toLocaleString('vi-VN'); } @@ -77,13 +77,13 @@ export default function ListingsPage() { {/* Header */}
-

Quan ly tin dang

+

Quản lý tin đăng

- Quan ly, theo doi va cap nhat cac tin dang cua ban + Quản lý, theo dõi và cập nhật các tin đăng của bạn

- +
@@ -91,13 +91,13 @@ export default function ListingsPage() {
- Tong tin dang + Tổng tin đăng {loading ? '...' : stats.total} - Dang hoat dong + Đang hoạt động {loading ? '...' : stats.active} @@ -105,7 +105,7 @@ export default function ListingsPage() { - Cho duyet + Chờ duyệt {loading ? '...' : stats.pending} @@ -113,7 +113,7 @@ export default function ListingsPage() { - Tong luot xem + Tổng lượt xem {loading ? '...' : stats.views} @@ -128,7 +128,7 @@ export default function ListingsPage() { } className="w-40" > - + {TRANSACTION_TYPES.map((t) => ( + {PROPERTY_TYPES.map((t) => ( + {Object.entries(LISTING_STATUSES).map(([key, { label }]) => (
@@ -187,10 +187,10 @@ export default function ListingsPage() {
) : !result || result.data.length === 0 ? (
-

Chua co tin dang nao

+

Chưa có tin đăng nào

@@ -228,7 +228,7 @@ export default function ListingsPage() {

- {listing.property.areaM2} m2 + {listing.property.areaM2} m² {listing.property.bedrooms != null && ( @@ -242,9 +242,9 @@ export default function ListingsPage() { )}
- {listing.viewCount} luot xem - {listing.inquiryCount} lien he - {listing.saveCount} da luu + {listing.viewCount} lượt xem + {listing.inquiryCount} liên hệ + {listing.saveCount} đã lưu
@@ -259,14 +259,14 @@ export default function ListingsPage() {
QuanLoai BDSGia trung viGia/m2Tin dangNgay banQuậnLoại BĐSGiá trung vịGiá/m²Tin đăngNgày bán YoY
- - - - - - - - + + + + + + + + @@ -311,7 +311,7 @@ export default function ListingsPage() { - + @@ -338,7 +338,7 @@ export default function ListingsPage() { disabled={filters.page <= 1} onClick={() => setFilters((f) => ({ ...f, page: f.page - 1 }))} > - Truoc + Trước Trang {result.page} / {result.totalPages} @@ -349,7 +349,7 @@ export default function ListingsPage() { disabled={filters.page >= result.totalPages} onClick={() => setFilters((f) => ({ ...f, page: f.page + 1 }))} > - Tiep + Tiếp )} diff --git a/apps/web/app/(public)/listings/[id]/page.tsx b/apps/web/app/(public)/listings/[id]/page.tsx index d153d7a..aa7e3c3 100644 --- a/apps/web/app/(public)/listings/[id]/page.tsx +++ b/apps/web/app/(public)/listings/[id]/page.tsx @@ -17,7 +17,7 @@ const ListingMap = dynamic( ssr: false, loading: () => (
-

Dang tai ban do...

+

Đang tải bản đồ...

), }, @@ -45,7 +45,7 @@ export default function PublicListingDetailPage() { listingsApi .getById(id) .then(setListing) - .catch((err) => setError(err instanceof Error ? err.message : 'Khong tai duoc tin dang')) + .catch((err) => setError(err instanceof Error ? err.message : 'Không tải được tin đăng')) .finally(() => setLoading(false)); }, [id]); @@ -74,9 +74,9 @@ export default function PublicListingDetailPage() { -

{error || 'Khong tim thay tin dang'}

+

{error || 'Không tìm thấy tin đăng'}

- + ); @@ -90,9 +90,9 @@ export default function PublicListingDetailPage() {
{/* Breadcrumb */} @@ -121,12 +121,12 @@ export default function PublicListingDetailPage() {

{formatPrice(listing.priceVND)} VND

{listing.pricePerM2 != null && (

- ~{listing.pricePerM2.toLocaleString('vi-VN')} VND/m2 + ~{listing.pricePerM2.toLocaleString('vi-VN')} VND/m²

)} {listing.rentPriceMonthly && (

- Thue: {formatPrice(listing.rentPriceMonthly)}/thang + Thuê: {formatPrice(listing.rentPriceMonthly)}/tháng

)}
@@ -137,18 +137,18 @@ export default function PublicListingDetailPage() { {/* Quick specs bar */}
- + {property.bedrooms != null && ( - + )} {property.bathrooms != null && ( - + )} {property.floors != null && ( - + )} {property.direction && ( - + )}
@@ -158,7 +158,7 @@ export default function PublicListingDetailPage() { {/* Description */} - Mo ta + Mô tả

{property.description}

@@ -168,19 +168,19 @@ export default function PublicListingDetailPage() { {/* Details */} - Thong tin chi tiet + Thông tin chi tiết
- - - - - - - - - + + + + + + + + +
@@ -189,7 +189,7 @@ export default function PublicListingDetailPage() { {property.amenities && property.amenities.length > 0 && ( - Tien ich + Tiện ích
@@ -206,7 +206,7 @@ export default function PublicListingDetailPage() { {/* Map */} - Vi tri tren ban do + Vị trí trên bản đồ - Lien he + Liên hệ
@@ -242,22 +242,22 @@ export default function PublicListingDetailPage() { - Goi ngay + Gọi ngay {agent && (
-

Moi gioi

+

Môi giới

{agent.agency &&

{agent.agency}

} {listing.commissionPct != null && ( -

Hoa hong: {listing.commissionPct}%

+

Hoa hồng: {listing.commissionPct}%

)}
)} @@ -270,20 +270,20 @@ export default function PublicListingDetailPage() {

{listing.viewCount}

-

Luot xem

+

Lượt xem

{listing.saveCount}

-

Luot luu

+

Lượt lưu

{listing.inquiryCount}

-

Lien he

+

Liên hệ

{listing.publishedAt && (

- Dang ngay {new Date(listing.publishedAt).toLocaleDateString('vi-VN')} + Đăng ngày {new Date(listing.publishedAt).toLocaleDateString('vi-VN')}

)} diff --git a/apps/web/app/(public)/search/page.tsx b/apps/web/app/(public)/search/page.tsx index 2310b3c..8b82a32 100644 --- a/apps/web/app/(public)/search/page.tsx +++ b/apps/web/app/(public)/search/page.tsx @@ -14,7 +14,7 @@ const ListingMap = dynamic( ssr: false, loading: () => (
-

Dang tai ban do...

+

Đang tải bản đồ...

), }, diff --git a/apps/web/components/charts/district-bar-chart.tsx b/apps/web/components/charts/district-bar-chart.tsx index 833fc11..5001e49 100644 --- a/apps/web/components/charts/district-bar-chart.tsx +++ b/apps/web/components/charts/district-bar-chart.tsx @@ -27,8 +27,8 @@ export function DistrictBarChart({ tooltipFormatter, }: DistrictBarChartProps) { const defaultFormatter: TooltipFormatter = (value, name) => [ - name === dataKey ? `${value} tr/m2` : String(value), - name === dataKey ? 'Gia' : 'Tin dang', + name === dataKey ? `${value} tr/m²` : String(value), + name === dataKey ? 'Giá' : 'Tin đăng', ]; return ( diff --git a/apps/web/components/charts/price-trend-chart.tsx b/apps/web/components/charts/price-trend-chart.tsx index afe9ee7..1c7dbe6 100644 --- a/apps/web/components/charts/price-trend-chart.tsx +++ b/apps/web/components/charts/price-trend-chart.tsx @@ -12,7 +12,7 @@ import { } from 'recharts'; interface PriceTrendChartProps { - data: { period: string; 'Gia/m2': number; 'Tin dang': number }[]; + data: { period: string; 'Gia/m2': number; 'Tin đăng': number }[]; height?: number; } @@ -37,7 +37,7 @@ export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) { fontSize: '0.875rem', }} formatter={(value, name) => [ - name === 'Gia/m2' ? `${value} tr/m2` : value, + name === 'Gia/m2' ? `${value} tr/m²` : value, name, ]} /> @@ -54,7 +54,7 @@ export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) {
Tin dangLoaiGiaDien tichTrang thaiLuot xemLien heNgay dangTin đăngLoạiGiáDiện tíchTrạng tháiLượt xemLiên hệNgày đăng
{formatPrice(listing.priceVND)} {listing.property.areaM2} m2{listing.property.areaM2} m²