Menu
ESC

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

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

Đang tải...

Bài 25 — Internationalization (i18n) & Localization (l10n)

Viết PRD & Product Spec cho PM Bài 25/60

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

Hãy hình dung bạn là PM của một sản phẩm fintech đang ăn nên làm ra ở Việt Nam. Một sáng đẹp trời, sếp gõ cửa: "Quý sau mình mở rộng sang Thái Lan và Indonesia. Bạn viết spec cho team chuẩn bị nhé." Bạn về bàn, mở PRD ra, và bắt đầu thấy gai người. Số tiền hiển thị thế nào — "1.000.000 ₫" hay "Rp 1.000.000" hay "฿1,000.00"? Ngày tháng ghi ra sao — Indonesia dùng dd/mm/yyyy còn Mỹ thì mm/dd/yyyy. Cái nút "Đặt hàng" dài 8 ký tự tiếng Việt, dịch sang tiếng Đức thành "Jetzt bestellen" tràn ra ngoài khung. Và nếu một ngày sản phẩm vào thị trường Trung Đông, toàn bộ giao diện phải lật ngược từ phải sang trái.

Đây chính là lý do i18n (internationalization) và l10n (localization) phải nằm trong spec ngay từ đầu, chứ không phải là việc "để sau khi xong v1 rồi tính". Lý do rất đơn giản: chi phí sửa một sản phẩm chưa được internationalize hóa sau khi đã build xong gần như tương đương viết lại. Mỗi chuỗi văn bản hard-code trong code, mỗi định dạng ngày tháng cứng nhắc, mỗi layout giả định tiếng Anh — tất cả đều trở thành nợ kỹ thuật khổng lồ khi bạn muốn mở rộng thị trường.

Với một PM Việt Nam, kỹ năng này còn quan trọng gấp đôi. Sản phẩm Việt thường bắt đầu nội địa, nhưng tham vọng "go regional" sang Đông Nam Á ngày càng phổ biến — Shopee, Grab, MoMo, VNG đều đã đi con đường này. Một spec biết nghĩ đến i18n/l10n từ ngày đầu sẽ tiết kiệm cho công ty hàng tháng trời và hàng tỷ đồng khi thời điểm mở rộng đến. Bài này dạy bạn cách viết phần i18n/l10n trong PRD/Spec sao cho engineer biết phải chuẩn bị gì, và team localization biết phải làm gì.

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

i18n và l10n khác nhau ở đâu

Hai từ này thường bị dùng lẫn lộn, nhưng chúng là hai công việc khác nhau, do hai nhóm người khác nhau làm, ở hai thời điểm khác nhau.

i18n — Internationalization (viết tắt là i18n vì giữa chữ "i" và chữ "n" có 18 ký tự). Đây là công việc kỹ thuật, do engineer làm. Bản chất là chuẩn bị code sao cho nó có khả năng hỗ trợ nhiều ngôn ngữ và vùng miền mà không cần sửa lại code. Một sản phẩm được i18n tốt nghĩa là:

  • Mọi chuỗi văn bản hiển thị cho người dùng đều được externalize — tách ra khỏi code, đưa vào file tài nguyên (resource file) riêng, không hard-code lẫn trong logic.
  • Code không giả định về độ dài chuỗi, hướng đọc, định dạng số/ngày/tiền tệ.
  • Hỗ trợ RTL (right-to-left) cho các ngôn ngữ viết từ phải sang trái như tiếng Ả Rập, Do Thái.
  • Dùng Unicode (UTF-8) để chứa được mọi ký tự, từ dấu tiếng Việt đến chữ Hán, Thái.
i18n là việc làm một lần. Bạn internationalize code một lần, sau đó thêm bao nhiêu ngôn ngữ cũng được mà không đụng vào code lõi.

l10n — Localization (giữa "l" và "n" có 10 ký tự). Đây là công việc thích ứng văn hóa và ngôn ngữ, do dịch giả, copywriter, và chuyên gia thị trường làm. Khi code đã sẵn sàng (đã i18n), l10n là quá trình thực sự dịch và điều chỉnh cho từng thị trường cụ thể:

  • Dịch nội dung (translation).
  • Điều chỉnh định dạng ngày tháng, số, tiền tệ theo locale.
  • Thích ứng văn hóa: màu sắc, hình ảnh, ví dụ, giọng điệu, thậm chí cả tính năng.
  • Tuân thủ quy định pháp lý địa phương.
l10n làm lặp lại cho mỗi thị trường mới. Mở sang Thái là một lần l10n, mở sang Indonesia là một lần l10n nữa.

Một câu để nhớ: i18n là xây cái khung có sẵn các lỗ cắm; l10n là cắm nội dung từng nước vào những lỗ đó.

Locale — đơn vị cốt lõi cần spec

Khái niệm trung tâm bạn phải đưa vào spec là locale. Một locale không chỉ là ngôn ngữ, mà là cặp ngôn ngữ + vùng miền, ví dụ vi-VN (tiếng Việt, Việt Nam), en-US (tiếng Anh, Mỹ), en-GB (tiếng Anh, Anh), zh-Hans-CN (tiếng Trung giản thể, Trung Quốc).

Tại sao phải tách ngôn ngữ và vùng miền? Vì cùng một ngôn ngữ có thể có định dạng khác nhau theo vùng. Tiếng Anh ở Mỹ viết ngày 06/27/2026, ở Anh viết 27/06/2026. Cùng tiếng Tây Ban Nha nhưng es-ES (Tây Ban Nha) và es-MX (Mexico) khác nhau về từ vựng và tiền tệ. Khi spec một tính năng, bạn phải nói rõ sản phẩm hỗ trợ những locale nào, chứ không chỉ "những ngôn ngữ nào".

Những thứ thay đổi theo locale (locale-sensitive)

Đây là danh sách bạn cần liệt kê trong spec để engineer không bỏ sót:

  • Văn bản UI: mọi nhãn, nút, thông báo, lỗi.
  • Số: dấu phân cách hàng nghìn và thập phân. Việt Nam viết 1.000.000,50, Mỹ viết 1,000,000.50.
  • Tiền tệ: ký hiệu, vị trí (100₫ hay $100), số chữ số thập phân (VND không có phần lẻ, USD có 2 chữ số).
  • Ngày & giờ: thứ tự, định dạng 12h/24h, múi giờ, lịch (một số nước dùng lịch khác Gregorian).
  • Số nhiều (pluralization): tiếng Anh có "1 item" / "2 items"; tiếng Ả Rập có tới 6 dạng số nhiều; tiếng Việt may mắn không chia số nhiều.
  • Hướng đọc: LTR (left-to-right) hay RTL.
  • Sắp xếp (collation): thứ tự bảng chữ cái khác nhau giữa các ngôn ngữ.
  • Tên & địa chỉ: thứ tự họ-tên, định dạng địa chỉ, mã bưu chính.
  • Nội dung văn hóa: hình ảnh, biểu tượng, màu sắc, ví dụ, danh xưng.

Tình huống thực tế

Ví dụ 1 — MoMo mở rộng tính năng cho người dùng nói tiếng Anh

Giả định MoMo muốn phục vụ nhóm expat (người nước ngoài sống ở Việt Nam) và khách du lịch bằng giao diện tiếng Anh. PM viết spec cho tính năng "chuyển tiền", và đây là chỗ một spec yếu sẽ vấp ngã.

Spec yếu chỉ ghi: "Hỗ trợ tiếng Anh cho màn hình chuyển tiền." Engineer dịch chuỗi, ship, rồi phát sinh hàng loạt vấn đề. Màn hình hiển thị "Số dư: 1.500.000₫" — nhưng với người dùng en-US, dấu chấm bị hiểu nhầm là dấu thập phân, họ tưởng mình chỉ có 1,5 đồng. Thông báo "Bạn đã chuyển 2 lần hôm nay" dịch thành "You transferred 2 time today" vì không xử lý số nhiều.

Spec tốt sẽ ghi rõ: hệ thống hỗ trợ hai locale vi-VNen-US; số tiền VND luôn hiển thị theo định dạng locale đang chọn (1.500.000 ₫ cho vi-VN, 1,500,000 ₫ cho en-US) nhưng đơn vị tiền tệ giữ nguyên VND vì giao dịch vẫn bằng đồng — đây là điểm tinh tế: localize định dạng nhưng không đổi loại tiền. Spec cũng nêu chuỗi "đã chuyển {count} lần" phải dùng cơ chế pluralization với khóa transfer_count có dạng số ít và số nhiều cho tiếng Anh.

Bài học: Localize định dạng hiển thị không có nghĩa là đổi bản chất dữ liệu. Spec phải phân biệt rõ "hiển thị thế nào" và "lưu/giao dịch bằng gì".

Ví dụ 2 — Shopee và bài toán Đông Nam Á đa thị trường

Shopee là ví dụ kinh điển về sản phẩm phải i18n từ gốc vì sinh ra để phủ cả khu vực: Việt Nam, Thái Lan, Indonesia, Malaysia, Philippines, Singapore, Đài Loan. Mỗi nước là một locale với tiền tệ riêng (VND, THB, IDR, MYR, PHP, SGD, TWD), ngôn ngữ riêng, và quy định riêng.

Hãy tưởng tượng PM spec tính năng "khuyến mãi 11.11". Nếu spec chỉ viết bằng tư duy một thị trường, sẽ vỡ ngay. Giá "9.000.000đ" ở Việt Nam là số tiền lớn, nhưng "9.000.000 IDR" ở Indonesia chỉ tương đương vài trăm nghìn đồng — bố cục giao diện phải chịu được con số rất dài (đồng Rupiah có nhiều số 0). Tên tháng "Tháng 11" với người Thái phải hiển thị theo lịch Phật (năm 2569 thay vì 2026 nếu dùng lịch Phật). Banner khuyến mãi có chữ trên ảnh — nếu chữ nung cứng vào ảnh (baked into image) thì mỗi nước phải thiết kế lại ảnh, thay vì để chữ là layer text có thể dịch.

Một spec Shopee tốt sẽ yêu cầu: text không được baked vào ảnh banner; mọi giá trị tiền hiển thị qua hàm format theo locale; layout component giá phải test với chuỗi dài nhất (IDR). Spec cũng phải định nghĩa fallback locale — nếu một chuỗi chưa được dịch sang tiếng Thái thì hiển thị tiếng Anh thay vì hiện ra khóa kỹ thuật như promo.title.1111.

Bài học: Với sản phẩm đa thị trường, spec phải coi locale là một biến số, và phải định nghĩa hành vi fallback khi bản dịch thiếu. Đừng bao giờ để người dùng nhìn thấy translation key.

Ví dụ 3 — Startup SaaS Việt vào thị trường Trung Đông và cú sốc RTL

Một startup SaaS Việt Nam (giả định là công cụ quản lý dự án như Base.vn) ký được hợp đồng lớn với khách hàng ở UAE, yêu cầu giao diện tiếng Ả Rập. Đây là lúc i18n yếu phơi bày toàn bộ.

Tiếng Ả Rập đọc từ phải sang trái. Toàn bộ giao diện phải mirror (lật gương): sidebar chuyển sang phải, mũi tên "next" phải chỉ sang trái, icon, padding, alignment đều đảo. Nếu code đã hard-code margin-left: 16px ở khắp nơi thay vì dùng thuộc tính logic margin-inline-start, thì đội kỹ thuật phải dò từng dòng CSS. Đây chính là phần i18n lẽ ra phải làm từ đầu.

Spec tốt cho tình huống này phải nêu: sản phẩm hỗ trợ thuộc tính dir="rtl" ở cấp root; mọi CSS dùng logical properties (inline-start/inline-end thay cho left/right); icon có hướng (mũi tên, dấu tích chỉ thị tiến trình) phải có biến thể RTL; số trong tiếng Ả Rập có thể dùng chữ số Đông Ả Rập (٠١٢٣) và spec phải quyết định dùng dạng nào.

Bài học: RTL là phép thử khắc nghiệt nhất cho chất lượng i18n. Nếu spec ngay từ v1 yêu cầu dùng logical CSS properties, thì khi RTL đến, công việc chỉ là dịch chứ không phải đại phẫu. Đây đúng tinh thần "i18n chuẩn bị, l10n thực thi".

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

Đây là cách bạn viết phần i18n/l10n trong PRD/Spec một cách bài bản.

Bước 1 — Khai báo phạm vi locale. Mở đầu phần này, liệt kê rõ các locale được hỗ trợ ở v1 và những locale dự kiến cho tương lai. Ví dụ: "v1 hỗ trợ vi-VN (mặc định) và en-US. Roadmap: th-TH, id-ID ở quý sau." Định rõ locale mặc địnhlocale fallback.

Bước 2 — Đánh dấu i18n là yêu cầu kỹ thuật bắt buộc. Ghi rõ ngay cả khi v1 chỉ có một ngôn ngữ, code vẫn phải được internationalize: externalize toàn bộ chuỗi, dùng thư viện format theo locale (như ICU MessageFormat, react-intl, hoặc i18next), dùng logical CSS. Đây là yêu cầu "non-negotiable" để tránh nợ kỹ thuật.

Bước 3 — Liệt kê dữ liệu locale-sensitive trong tính năng. Đi qua từng màn hình của tính năng và đánh dấu mọi chỗ thay đổi theo locale: tiền tệ, ngày giờ, số, số nhiều, chuỗi động. Lập một bảng nhỏ liệt kê từng phần tử và cách nó được format.

Bước 4 — Định nghĩa xử lý chuỗi động và pluralization. Với những chuỗi có biến chèn vào (như "Bạn có {n} thông báo"), nêu rõ cấu trúc và quy tắc số nhiều. Tránh ghép chuỗi thủ công (string concatenation) vì trật tự từ thay đổi theo ngôn ngữ — phải dùng template có placeholder.

Bước 5 — Spec hành vi fallback và thiếu bản dịch. Quy định: nếu chuỗi chưa có bản dịch cho locale hiện tại, hiển thị bản dịch của locale fallback (thường là tiếng Anh), tuyệt đối không hiện translation key. Nêu rõ cách hệ thống ghi log để team localization biết chuỗi nào còn thiếu.

Bước 6 — Spec ràng buộc layout. Nêu yêu cầu UI chịu được text dài (đức/đức ngữ thường dài hơn 30%), không cắt chữ giữa chừng, không baked text vào ảnh, và sẵn sàng RTL nếu có trong roadmap.

Bước 7 — Định nghĩa quy trình l10n. Mô tả nội dung được đưa cho dịch giả thế nào (file resource, công cụ TMS như Crowdin/Lokalise/Phrase), ai review chất lượng bản dịch, và cách bản dịch quay lại sản phẩm. Đây là phần l10n thực thi.

Bước 8 — Định nghĩa tiêu chí chấp nhận liên quan i18n. Thêm acceptance criteria kiểm thử được, ví dụ: "Khi đổi locale sang en-US, mọi số tiền hiển thị đúng định dạng dấu phẩy; không có chuỗi nào hiện ra dưới dạng key."

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

Lỗi 1 — Hard-code chuỗi trong code. Đây là tội phổ biến nhất. Engineer viết thẳng "Đặt hàng" vào component. Khi cần dịch, phải lục từng file. Spec phải yêu cầu externalize ngay từ commit đầu.

Lỗi 2 — Ghép chuỗi thủ công. Viết "Bạn có " + count + " tin nhắn". Khi dịch sang ngôn ngữ có trật tự từ khác, câu trở nên vô nghĩa, và không xử lý được số nhiều. Mẹo: luôn dùng template hoàn chỉnh với placeholder, mỗi câu là một khóa độc lập.

Lỗi 3 — Giả định một chuỗi dịch ra cùng độ dài. "OK" tiếng Anh, dịch sang ngôn ngữ khác có thể dài gấp ba. Mẹo: test giao diện với "pseudo-localization" — thay thế tạm bằng chuỗi dài có dấu để lộ ra chỗ tràn layout trước khi có bản dịch thật.

Lỗi 4 — Baked text vào hình ảnh. Mỗi lần đổi ngôn ngữ phải làm lại ảnh. Mẹo: tách text khỏi ảnh, để là layer văn bản localize được.

Lỗi 5 — Quên múi giờ và lịch. Hiển thị "20:00" mà không nói múi giờ nào khiến người dùng nhầm. Mẹo: lưu thời gian dạng UTC, hiển thị theo timezone của người dùng, và spec rõ điều này.

Lỗi 6 — Nhầm "dịch là đủ". Localization còn là thích ứng văn hóa: ví dụ trong tài liệu, danh xưng, hình ảnh, thậm chí cả quy định pháp lý. Mẹo: trong spec, tách riêng "translation" và "cultural adaptation" để team không nghĩ chỉ cần Google Translate.

Mẹo vàng: Hãy coi tiếng Việt với dấu là phép thử i18n đầu tiên. Nếu sản phẩm xử lý đúng "Nguyễn Thị Hồng Nhung" với đầy đủ dấu, sắp xếp đúng, hiển thị không lỗi font — đó là dấu hiệu nền tảng Unicode đã vững. Ngược lại nếu còn thấy "Nguy?n Th? H?ng" thì i18n căn bản đã hỏng.

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

Chọn một tính năng quen thuộc — ví dụ màn hình "Lịch sử giao dịch" của một ví điện tử. Hãy viết phần i18n/l10n trong spec cho nó, gồm:

  • Khai báo locale: liệt kê locale v1 (vi-VN, en-US) và roadmap (th-TH, id-ID). Ghi rõ default và fallback.
  • Bảng locale-sensitive: lập bảng 3 cột (Phần tử | Loại dữ liệu | Cách format theo locale) cho ít nhất 5 phần tử: số tiền giao dịch, ngày giao dịch, giờ, số lượng giao dịch, trạng thái.
  • Một chuỗi động có pluralization: viết spec cho dòng "Hiển thị {n} giao dịch", kèm quy tắc số ít/số nhiều cho tiếng Anh và lưu ý tiếng Việt không chia.
  • Hành vi fallback: viết 2 câu quy định điều gì xảy ra khi chuỗi tiếng Thái chưa được dịch.
  • 2 acceptance criteria kiểm thử được liên quan đến i18n.
Sau khi viết xong, tự kiểm bằng câu hỏi: "Một engineer chưa từng làm i18n đọc spec này, họ có biết phải externalize chuỗi và dùng hàm format theo locale không?" Nếu chưa rõ, viết lại cho rõ hơn.

Tóm tắt

  • i18n (internationalization) là việc kỹ thuật, làm một lần: chuẩn bị code để có thể localize — externalize chuỗi, dùng Unicode, logical CSS, hỗ trợ RTL, format theo locale. l10n (localization) là việc thích ứng, lặp lại mỗi thị trường: dịch nội dung và điều chỉnh văn hóa, định dạng, pháp lý.
  • Locale = ngôn ngữ + vùng miền (vi-VN, en-US), là đơn vị bạn phải spec, không chỉ "ngôn ngữ".
  • Trong spec phải liệt kê rõ mọi thứ locale-sensitive: tiền tệ, số, ngày giờ, số nhiều, hướng đọc, sắp xếp, văn hóa.
  • Luôn định nghĩa locale mặc định, fallback, và hành vi khi thiếu bản dịch — không bao giờ để người dùng thấy translation key.
  • Phân biệt rõ format hiển thịbản chất dữ liệu (như ví dụ MoMo: đổi cách hiển thị số tiền nhưng không đổi loại tiền tệ).
  • Yêu cầu i18n là bắt buộc ngay cả khi v1 chỉ một ngôn ngữ, để tránh chi phí viết lại sau này — như bài học RTL của startup Việt vào Trung Đông.
  • Tránh các lỗi kinh điển: hard-code chuỗi, ghép chuỗi thủ công, baked text vào ảnh, giả định độ dài chuỗi, quên múi giờ.
Một PRD biết nghĩ đến i18n/l10n từ ngày đầu là tấm vé để sản phẩm Việt vươn ra khu vực và thế giới mà không phải trả giá bằng một cuộc đại phẫu code.