Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 26 — Plural và Number Format — i18n Pitfalls

Mở đầu — vì sao bài này quan trọng

Bạn viết một dòng microcopy đơn giản: "You have 3 new messages". Dev bọc nó vào code: You have {count} new message${count > 1 ? 's' : ''}. Chạy trên bản tiếng Anh — hoàn hảo. Rồi sản phẩm mở rộng sang thị trường Việt Nam, Nhật, Nga, Ả Rập. Đột nhiên bạn nhận về những dòng như "Bạn có 1 tin nhắns", "1 sản phẩms trong giỏ", hoặc tệ hơn, cả một câu tiếng Việt bị lộn ngược nghĩa vì logic số nhiều của tiếng Anh bị nhét vào một ngôn ngữ không có số nhiều.

Đây là một trong những cái bẫy âm thầm và tốn kém nhất của UX writing khi sản phẩm đi quốc tế. Nó âm thầm vì trên màn hình tiếng Anh mọi thứ trông ổn, nên không ai để ý. Nó tốn kém vì khi lỗi lộ ra ở thị trường mới, bạn phải sửa lại toàn bộ chuỗi văn bản, đôi khi phải viết lại cả kiến trúc code xử lý chuỗi.

Là một UX writer hoặc content designer, bạn không phải là kỹ sư — nhưng bạn là người quyết định cách văn bản được viết ra và cách nó phải co giãn theo con số. Nếu bạn không hiểu cơ chế số nhiều (plural rules) và định dạng số (number format) khác nhau giữa các ngôn ngữ, bạn sẽ vô tình đặt ra những câu văn không thể dịch được. Bài này giúp bạn hiểu đủ sâu về mặt ngôn ngữ và kỹ thuật để viết microcopy "dịch được" (translatable) ngay từ đầu — đặc biệt là hiểu tại sao tiếng Việt lại là một trường hợp đặc biệt dễ chịu, còn tiếng Anh mới là kẻ gây rắc rối.

Khái niệm cốt lõi

Plural rules: mỗi ngôn ngữ có số "hình thái" khác nhau

Điều quan trọng nhất cần nhớ: số dạng số nhiều (plural forms) của một ngôn ngữ không phải là 1 hay 2 — nó là một con số do ngữ pháp quy định, và khác nhau rất nhiều.

Tổ chức Unicode (qua chuẩn CLDR — Common Locale Data Repository) phân loại mỗi ngôn ngữ theo các "plural category": zero, one, two, few, many, other. Không phải ngôn ngữ nào cũng dùng đủ 6 loại này.

  • Tiếng Anh: 2 dạng — one ("1 item") và other ("0 items", "2 items", "5 items"). Chú ý: số 0 dùng dạng số nhiều trong tiếng Anh ("0 items", không phải "0 item").
  • Tiếng Việt: 1 dạng duy nhất — chỉ other. Danh từ không biến đổi theo số. "1 sản phẩm", "2 sản phẩm", "100 sản phẩm" — chữ "sản phẩm" đứng yên. Nếu muốn nhấn mạnh số nhiều, tiếng Việt dùng từ đứng trước như "các", "những", "nhiều" chứ không đổi đuôi danh từ.
  • Tiếng Nga: 3-4 dạng (one, few, many, other) với quy tắc phức tạp dựa trên chữ số cuối. "1 файл", "2 файла", "5 файлов".
  • Tiếng Ả Rập: đủ 6 dạng — zero, one, two, few, many, other. Đây là ngôn ngữ dùng nhiều dạng nhất trong các ngôn ngữ phổ biến.
  • Tiếng Nhật, Hàn, Trung, Thái: giống tiếng Việt — chỉ 1 dạng other.
Bài học đầu tiên: đừng bao giờ giả định "singular + s". Cái công thức count === 1 ? 'item' : 'items' là một sự cẩu thả mã hóa cứng (hardcode) ngữ pháp tiếng Anh vào code. Nó sẽ vỡ ngay khi gặp ngôn ngữ khác.

Tiếng Việt: món quà và cái bẫy ngược

Tiếng Việt chỉ có một dạng số, nên nghe qua thì việc bản địa hóa (localization) sang tiếng Việt rất dễ — không phải lo về singular/plural. Đúng là như vậy về mặt danh từ.

Nhưng đây là cái bẫy ngược: khi dịch từ tiếng Việt hoặc viết chuỗi gốc bằng tiếng Việt để rồi dịch sang tiếng Anh, bạn dễ quên rằng ngôn ngữ đích cần phân biệt số nhiều. Nếu bạn thiết kế chuỗi kiểu "Bạn có {count} tin nhắn mới" và coi đó là chuẩn, khi hệ thống dịch tự động hoặc dịch giả xử lý sang tiếng Anh, họ phải bổ sung logic plural mà chuỗi gốc không hề gợi ý. Kết quả thường là bản tiếng Anh dịch thô, sai ngữ pháp số nhiều.

Ngoài ra, tiếng Việt có những từ chỉ số lượng riêng cần chú ý: loại từ (classifier) như "cái", "chiếc", "con", "quyển"... Ví dụ "3 cái áo", "2 con chó", "5 quyển sách". Khi count thay đổi, loại từ không đổi, nhưng khi bạn ghép chuỗi động, bạn phải đảm bảo loại từ đi kèm đúng danh từ — không thể dùng một placeholder chung chung "{count} {noun}" nếu mỗi noun cần một loại từ khác nhau.

Number format: dấu phân cách và tiền tệ

Song song với plural là định dạng số. Cùng một con số một triệu rưỡi:

  • Tiếng Anh (Mỹ): 1,500,000.50 — phẩy ngăn hàng nghìn, chấm ngăn thập phân.
  • Tiếng Việt: 1.500.000,50 — chấm ngăn hàng nghìn, phẩy ngăn thập phân (theo chuẩn chính thức, dù thực tế nhiều người vẫn dùng dấu chấm cho thập phân).
  • Tiếng Đức: 1.500.000,50 — giống Việt Nam.
  • Tiếng Ấn Độ: 15,00,000.50 — nhóm theo hệ lakh/crore, khác hẳn.
Về tiền tệ: vị trí ký hiệu, khoảng trắng, số chữ số thập phân đều khác nhau. "$1,500.00" trong tiếng Anh Mỹ; "1.500.000 ₫" trong tiếng Việt (đồng thường không dùng số lẻ thập phân, và ký hiệu ₫ đứng sau, có khoảng trắng). Nếu bạn hardcode "$" hoặc format số theo kiểu Mỹ trong microcopy, bản Việt sẽ trông rất kỳ.

Nguyên tắc vàng: không nối chuỗi thủ công (no string concatenation)

Cái sai gốc rễ khiến mọi thứ vỡ là nối chuỗi: "Bạn có " + count + " tin nhắn". Cách này giả định trật tự từ, dạng số, và định dạng số của một ngôn ngữ cụ thể.

Giải pháp chuẩn công nghiệp là dùng thư viện ICU MessageFormat (International Components for Unicode). Nó cho phép viết một message có "nhánh" plural, và mỗi ngôn ngữ tự chọn nhánh phù hợp theo CLDR. Ví dụ cú pháp ICU:

{count, plural,
  one {Bạn có # tin nhắn mới}
  other {Bạn có # tin nhắn mới}
}

Với tiếng Việt, cả hai nhánh giống nhau (hoặc chỉ cần other). Với tiếng Anh, one là "1 message" và other là "# messages". Dấu # tự động chèn con số đã được định dạng đúng theo locale. Vai trò của bạn — UX writer — là cung cấp bản dịch cho từng nhánh mà ngôn ngữ đó cần, chứ không phải viết một câu cứng.

Tình huống thực tế

Ví dụ 1: Shopee và cái giỏ hàng "1 sản phẩms"

Bối cảnh: Một nền tảng thương mại điện tử kiểu Shopee/Lazada xây dựng bản gốc bằng tiếng Anh cho đội kỹ thuật ở Singapore. Component giỏ hàng hiển thị: ${count} item${count !== 1 ? 's' : ''} in your cart. Khi mở rộng sang Việt Nam, đội dev không dịch lại logic — họ chỉ thay chuỗi cơ sở thành "sản phẩm" nhưng giữ nguyên đoạn nối "s". Kết quả trên bản tiếng Việt: người dùng thấy "2 sản phẩms trong giỏ".

Diễn giải: Vấn đề không phải ở bản dịch từ vựng — "sản phẩm" đúng nghĩa. Vấn đề là logic plural của tiếng Anh (thêm "s") bị hardcode nằm ngoài chuỗi dịch được. Dịch giả tiếng Việt không có cách nào chạm tới đoạn "s" đó vì nó nằm trong code, không nằm trong file dịch. Khi bị người dùng phản ánh, đội phải refactor: gỡ toàn bộ logic "+s" ra khỏi code, đưa cả câu vào ICU MessageFormat, để tiếng Việt chỉ dùng nhánh other không có "s".

Bài học: Bất kỳ biến đổi ngữ pháp nào theo số lượng — kể cả đơn giản như thêm "s" — phải nằm trong lớp bản dịch, không nằm trong code. Nếu là UX writer, khi bạn thấy dev viết + 's' trong code, đó là red flag cần cảnh báo ngay.

Ví dụ 2: Ứng dụng ngân hàng số và con số "1.500.000"

Bối cảnh: Một ứng dụng fintech Việt Nam (giả định tên "VíSố") hiển thị số dư và lịch sử giao dịch. Bản đầu tiên dùng thư viện JavaScript mặc định toLocaleString() nhưng quên set locale, nên nó chạy theo locale của server (en-US). Người dùng Việt Nam thấy số dư "1,500,000 ₫" với dấu phẩy — trong khi ở Việt Nam dấu phẩy thường được hiểu là dấu thập phân. Một số người dùng lớn tuổi tưởng số dư của mình chỉ còn "1,5 triệu lẻ" hay hiểu nhầm thành 1,5 đồng.

Thêm một lỗi microcopy đi kèm: dòng thông báo "Bạn vừa nhận {count} giao dịch mới" được viết cứng, nhưng ở màn hình tiếng Anh nó là "You received {count} new transaction(s)" — đội dùng dấu "(s)" như một cách né plural. Trên tiếng Việt lại dịch thành "giao dịch(s)" vì copy-paste nguyên placeholder.

Diễn giải: Có hai lỗi cùng lúc — number format sai locale, và plural bị "né" bằng "(s)" một cách cẩu thả. Trong lĩnh vực ngân hàng, hiểu sai con số không chỉ khó chịu mà còn gây mất niềm tin nghiêm trọng. Đội phải chuẩn hóa: format mọi số tiền qua Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }), và bỏ hoàn toàn "(s)".

Bài học: Với ngôn ngữ chỉ có một dạng số như tiếng Việt, đừng bao giờ dùng chiêu "(s)" hay "item(s)" trong bản gốc rồi hy vọng dịch được — nó bẩn ở mọi ngôn ngữ. Và luôn khai báo locale rõ ràng khi format số, đừng phó mặc mặc định.

Ví dụ 3: App học tiếng và bài toán "còn 2 ngày"

Bối cảnh: Một ứng dụng edtech (giả định "HọcMỗiNgày") gửi thông báo nhắc streak học tập. Bản tiếng Việt: "Bạn còn {days} ngày để giữ chuỗi học". Rất gọn vì tiếng Việt không đổi "ngày". Nhưng khi mở rộng sang thị trường Nga và Ả Rập, đội phát hiện chuỗi này không thể dịch đúng: tiếng Nga cần 3 dạng khác nhau cho "1 день / 2 дня / 5 дней", tiếng Ả Rập cần tới 6 dạng, bao gồm cả dạng zero ("0 ngày") mang sắc thái riêng.

Vì chuỗi gốc được thiết kế theo tư duy tiếng Việt (một dạng), khi đưa vào hệ thống dịch, các nhánh plural cho tiếng Nga/Ả Rập bị bỏ trống hoặc dịch giả phải tự đoán. Đội phải làm lại: định nghĩa message theo ICU với đầy đủ khả năng phân nhánh, rồi để mỗi locale khai báo chỉ những nhánh nó cần. Tiếng Việt vẫn chỉ điền other; tiếng Nga điền one/few/many; tiếng Ả Rập điền đủ 6.

Diễn giải: Đây chính là "cái bẫy ngược" của tiếng Việt đã nói ở trên. Vì tiếng Việt quá dễ, đội quen viết chuỗi phẳng và quên rằng ngôn ngữ đích cần nhiều nhánh. Nếu ngay từ đầu chuỗi được đặt trong khung ICU MessageFormat, việc mở rộng sang Nga/Ả Rập chỉ là điền thêm nhánh, không phải viết lại toàn bộ.

Bài học: Ngay cả khi bạn chủ yếu viết tiếng Việt, hãy thiết kế microcopy như thể ngày mai nó sẽ được dịch sang một ngôn ngữ có 6 dạng số nhiều. Đó là tư duy "i18n-ready" (sẵn sàng quốc tế hóa).

Hướng dẫn từng bước

Đây là quy trình để viết một microcopy có con số mà không dính bẫy i18n:

Bước 1 — Nhận diện mọi biến số trong câu. Đọc chuỗi và khoanh tròn mọi chỗ có con số động: số lượng item, số ngày, số tiền, phần trăm. Mỗi con số là một điểm rủi ro cả về plural lẫn number format.

Bước 2 — Hỏi: câu có đổi ngữ pháp theo con số không? Trong tiếng Việt câu trả lời gần như luôn là "không" cho danh từ, nhưng hãy nghĩ tới ngôn ngữ đích. Nếu sản phẩm sẽ đa ngôn ngữ, mặc định giả định "có" và chuẩn bị khung plural.

Bước 3 — Đặt câu vào ICU MessageFormat, không nối chuỗi. Viết message dạng {count, plural, one {...} other {...}}. Dùng # để chèn con số thay vì lặp lại biến. Với tiếng Việt, thường chỉ cần nhánh other, nhưng khung phải sẵn để ngôn ngữ khác điền thêm.

Bước 4 — Tách định dạng số ra khỏi văn bản. Đừng viết "1.500.000" cứng trong câu. Dùng placeholder được format qua Intl.NumberFormat hoặc {amount, number} trong ICU, khai báo locale rõ ràng. Với tiền tệ, dùng style: 'currency' và mã tiền tệ.

Bước 5 — Xử lý trường hợp số 0 riêng nếu cần. "Bạn có 0 tin nhắn" đôi khi nên viết hẳn thành empty state "Bạn chưa có tin nhắn nào". ICU cho phép nhánh =0 để viết câu riêng cho số 0, tự nhiên hơn nhiều so với "0 tin nhắn".

Bước 6 — Viết bản dịch cho từng nhánh, không để dịch giả đoán. Nếu bạn quản lý file dịch, đảm bảo mỗi ngôn ngữ có đủ nhánh plural mà CLDR yêu cầu cho ngôn ngữ đó. Cung cấp ghi chú ngữ cảnh (context comment) để dịch giả biết count là gì.

Bước 7 — Test với các con số biên. Luôn kiểm thử với 0, 1, 2, 11, 21, 100, và một số rất lớn. Số 11 và 21 đặc biệt quan trọng cho tiếng Nga (11 dùng dạng many, không phải one dù kết thúc bằng 1).

Lỗi thường gặp & mẹo

Lỗi 1: Dùng "item(s)" hoặc "(s)" trong bản gốc. Đây là cách né plural lười biếng. Nó xấu ở tiếng Anh và vô nghĩa/bẩn khi dịch. Mẹo: thay bằng câu hoàn chỉnh qua plural nhánh, hoặc nếu thực sự không thể phân nhánh, viết lại câu để tránh danh từ đếm được ("Số lượng: {count}").

Lỗi 2: Nối chuỗi thủ công. "Bạn có " + n + " tin nhắn" giả định trật tự từ. Nhiều ngôn ngữ đảo con số ra sau. Mẹo: luôn dùng placeholder trong một message hoàn chỉnh, để dịch giả tự đặt vị trí {count} phù hợp ngữ pháp ngôn ngữ họ.

Lỗi 3: Hardcode dấu phân cách và ký hiệu tiền tệ. Viết cứng "$", "," hay "." là sai. Mẹo: luôn dùng Intl.NumberFormat với locale rõ ràng. Đừng dùng toLocaleString() không có tham số locale — nó phụ thuộc môi trường chạy.

Lỗi 4: Quên số 0 và số âm. "0 items" đúng ngữ pháp tiếng Anh (số nhiều), nhưng "Bạn có 0 mục" nghe cứng trong tiếng Việt. Số âm (ví dụ số dư âm trong app tài chính) cũng cần được định dạng và diễn đạt cẩn thận. Mẹo: cân nhắc empty state riêng cho 0, và kiểm tra hiển thị số âm.

Lỗi 5: Nghĩ tiếng Việt "một dạng" nghĩa là không cần lo gì. Tiếng Việt dễ về plural, nhưng chính sự dễ đó khiến đội thiết kế chuỗi phẳng, gây khó khi mở rộng. Mẹo: luôn đặt chuỗi vào khung ICU ngay từ ngôn ngữ gốc, kể cả khi hôm nay chỉ có tiếng Việt.

Lỗi 6: Quên loại từ (classifier) tiếng Việt. Ghép "{count} {noun}" chung chung dễ ra "3 áo" thay vì "3 chiếc áo". Mẹo: khi noun cần loại từ, đưa cả cụm "chiếc áo", "con chó" vào chuỗi dịch, đừng ghép rời từng mảnh.

Mẹo tổng quát: Nếu công cụ cho phép, dùng "pseudo-localization" — một chế độ giả lập dịch làm dài chuỗi và thêm ký tự đặc biệt — để phát hiện sớm chỗ nào bị nối chuỗi cứng hay bể layout trước khi có bản dịch thật.

Bài tập thực hành

Bài 1 — Sửa chuỗi bẩn. Bạn nhận được chuỗi gốc tiếng Anh trong code: You have {count} unread notification(s). Hãy: (a) viết lại nó dưới dạng ICU MessageFormat với nhánh oneother; (b) viết bản dịch tiếng Việt tương ứng; (c) giải thích vì sao bản Việt chỉ cần một nhánh.

Bài 2 — Thiết kế cho đa ngôn ngữ. Cho microcopy tiếng Việt: "Còn {days} ngày dùng thử". Giả định sản phẩm sắp mở rộng sang tiếng Anh và tiếng Nga. Hãy liệt kê các nhánh plural mà mỗi ngôn ngữ cần, và viết nội dung cho từng nhánh (tiếng Việt: 1 nhánh; tiếng Anh: 2 nhánh; tiếng Nga: bạn có thể tra CLDR — one/few/many).

Bài 3 — Number format. Một app fintech hiển thị số tiền 2500000.5. Viết ra cách nó phải hiển thị ở: (a) locale vi-VN (đồng VND), (b) locale en-US (USD). Chỉ rõ dấu phân cách hàng nghìn, dấu thập phân, vị trí ký hiệu tiền tệ, và số chữ số thập phân phù hợp cho từng loại tiền.

Bài 4 — Audit thực tế. Mở một ứng dụng bạn đang dùng (app ngân hàng, ví điện tử, hoặc TMĐT Việt Nam). Tìm 3 chỗ hiển thị con số kèm danh từ. Kiểm tra: chúng có dính "(s)", số nhiều tiếng Anh lạc, hay format số sai locale không? Ghi lại và đề xuất cách viết lại theo nguyên tắc bài này.

Tóm tắt

  • Mỗi ngôn ngữ có số dạng số nhiều (plural forms) khác nhau theo chuẩn CLDR: tiếng Anh 2 dạng, tiếng Việt 1 dạng, tiếng Nga 3-4, tiếng Ả Rập 6. Đừng bao giờ giả định "singular + s".
  • Tiếng Việt chỉ có một dạng số nên rất dễ về plural — nhưng chính sự dễ đó là cái bẫy: nó khiến ta viết chuỗi phẳng, gây vỡ khi mở rộng sang ngôn ngữ nhiều dạng.
  • Tuyệt đối tránh nối chuỗi thủ công và các chiêu né plural như "(s)" hay "item(s)". Dùng ICU MessageFormat với các nhánh plural, dùng # để chèn số.
  • Number format (dấu phân cách, thập phân, tiền tệ) khác nhau theo locale. Luôn dùng Intl.NumberFormat với locale khai báo rõ ràng, đừng hardcode dấu hay ký hiệu.
  • Xử lý riêng số 0 (thường nên là empty state), test với các con số biên (0, 1, 2, 11, 21, số lớn), và chú ý loại từ (classifier) khi ghép danh từ tiếng Việt.
  • Tư duy cốt lõi: viết mọi microcopy có con số như thể ngày mai nó sẽ được dịch sang một ngôn ngữ có 6 dạng số nhiều. Đó là microcopy "i18n-ready".
Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi