- Tác giả

- Name
- Nguyễn Đức Xinh
- Ngày xuất bản
- Ngày xuất bản
Japanese Text Handling in Software Development: Từ Full-width/Half-width Đến Database Và Testing
Executive Summary
Khi phát triển sản phẩm cho thị trường Nhật Bản, có một sự thật mà bất kỳ Developer hay QC nào cũng cần chấp nhận sớm: những giả định quen thuộc khi làm việc với text tiếng Anh không còn đúng nữa. Một ký tự không nhất thiết là một byte. Một input field không thể chỉ giới hạn bằng [A-Za-z0-9]. Một chuỗi "giống hệt nhau khi nhìn bằng mắt" có thể là hai giá trị hoàn toàn khác nhau trong database. Và một filter onKeyDown tưởng chừng vô hại có thể phá vỡ hoàn toàn trải nghiệm gõ tiếng Nhật của người dùng.
Bài viết này là một Japanese Text Handling Technical Guide dùng làm tài liệu tham khảo nội bộ cho Backend Developer, Frontend Developer, Fullstack Developer, QA/QC, Tech Lead/PL và Software Architect. Chúng ta sẽ đi xuyên suốt toàn bộ lifecycle của một chuỗi text tiếng Nhật, từ lúc người dùng gõ phím đến lúc dữ liệu nằm trong database, được search, được export ra Excel:
Keyboard / IME
↓
User Input
↓
Browser
↓
Frontend
↓
API / HTTP
↓
Backend
↓
Database
↓
Search / Sort / Compare
↓
Export / Import
↓
CSV / Excel / File
Sau khi đọc xong, bạn sẽ có khả năng: phân biệt các loại ký tự tiếng Nhật và biết khi nào nên cho phép từng loại trong từng input field; hiểu cơ chế IME để không viết code phá vỡ trải nghiệm gõ tiếng Nhật; thiết kế đúng chiến lược allow / reject / normalize / convert cho từng loại dữ liệu; nhận diện và debug được các lỗi encoding, 文字化け (mojibake), duplicate data do collation; và xây dựng được một bộ test case đủ rộng để QC không bỏ sót Japanese text edge case trước khi lên production.
Tại Sao Xử Lý Text Tiếng Nhật Lại Khó?
Phần lớn framework, thư viện validation, và thói quen coding phổ biến được thiết kế với giả định ngầm rằng text là ASCII, single-byte, không có khái niệm "chế độ nhập nhiều bước". Tiếng Nhật phá vỡ toàn bộ ba giả định đó:
- Không phải single-byte: một ký tự Hiragana/Katakana/Kanji chiếm 3 bytes trong UTF-8, khiến
strlen()(đếm byte) vàmb_strlen()(đếm ký tự) cho ra kết quả khác nhau hoàn toàn. - Không nhập một lần: người dùng gõ tiếng Nhật qua IME (Input Method Editor) — một chuỗi ký tự tạm thời (romaji hoặc kana thô) được "biên soạn" (compose) qua nhiều bước trước khi trở thành ký tự cuối cùng. Bắt sự kiện
keydownđể validate/filter input sẽ chặn nhầm quá trình này. - Không phải một representation duy nhất: cùng một khái niệm ("chữ A", "số 3", "katakana ア") có thể có nhiều dạng Unicode khác nhau — full-width, half-width, precomposed, decomposed — nhìn giống hệt nhau bằng mắt thường nhưng là các code point khác nhau, dẫn đến search/so sánh/database bị sai một cách âm thầm, không hề ném ra exception.
Chính vì lỗi loại này thường không crash, không log exception, mà chỉ âm thầm làm sai dữ liệu (hai category khác nhau bị gộp làm một, một khách hàng tìm không ra sản phẩm dù đã nhập đúng tên), nên nó nguy hiểm hơn nhiều so với một lỗi runtime thông thường — và đây chính là lý do bài viết này tồn tại.
Nền Tảng: Các Loại Ký Tự Tiếng Nhật
Hiragana
あ い う え お
か き く け こ
Hiragana là bộ chữ cái ngữ âm (phonetic) cơ bản nhất, dùng để viết từ thuần Nhật (native word), trợ từ ngữ pháp (particle: は, が, を, に), đuôi biến hình động từ/tính từ (okurigana), và toàn bộ văn bản khi người viết không nhớ Kanji. Đây là bộ ký tự bắt buộc phải hỗ trợ trong hầu như mọi input field liên quan đến tên riêng, địa chỉ, ghi chú tự do. Với các field như "furigana" (cách đọc của tên) — một trường cực kỳ phổ biến trong form Nhật Bản (ví dụ 名前: 田中太郎, フリガナ: たなか たろう) — Hiragana thường là loại ký tự duy nhất được cho phép, và validation pattern nên dùng Unicode script property thay vì liệt kê range thủ công:
/^\p{Script=Hiragana}+$/u
Katakana
ア イ ウ エ オ
カ キ ク ケ コ
Katakana dùng để viết từ mượn nước ngoài (パソコン = "personal computer"), tên nước ngoài, thuật ngữ khoa học, và — quan trọng với business system — trường "フリガナ" (furigana) trong rất nhiều hệ thống Nhật Bản yêu cầu Katakana chứ không phải Hiragana (ví dụ trường tên khách hàng trên hoá đơn ngân hàng, chứng từ hành chính).
Có một phân biệt quan trọng thường bị bỏ sót: Full-width Katakana và Half-width Katakana:
ア (full-width, U+30A2)
ア (half-width, U+FF71)
Hai ký tự này là hai Unicode code point hoàn toàn khác nhau, dù đọc giống nhau (đều là "a"). Half-width Katakana có nguồn gốc từ chuẩn cũ JIS X 0201 (dùng trong các hệ thống terminal/POS/máy fax đời cũ, và vẫn còn tồn tại trong dữ liệu hoá đơn convenience store hay legacy mainframe). Một hệ thống hiện đại hầu như luôn nên chuẩn hoá về Full-width Katakana khi lưu trữ, nhưng — như sẽ phân tích ở phần Full-width/Half-width — quyết định "convert" hay "giữ nguyên" phải dựa trên business requirement, không phải mặc định.
Kanji
東京
大阪
商品
会社
Kanji là chữ Hán được Nhật hoá, mang nghĩa (ideographic), không thể gõ trực tiếp bằng bàn phím mà phải qua IME chuyển đổi từ romaji/kana. Đây là lý do một input field tên công ty, tên sản phẩm, địa chỉ không bao giờ được validate bằng regex kiểu [A-Za-z]+ — điều này sẽ chặn hoàn toàn người dùng Nhật nhập đúng tên thật của họ. Một vấn đề nâng cao hơn là Kanji variant (異体字): cùng một chữ có thể có nhiều dạng Unicode hơi khác nhau về hình dạng (ví dụ 高 và 髙, 辺 và 邊/辺), thường gặp trong tên riêng của người lớn tuổi hoặc địa danh cổ. Với hệ thống thông thường, không cần xử lý đặc biệt; nhưng với hệ thống liên quan đến tên pháp lý (hộ tịch, hợp đồng), cần cân nhắc không tự động "sửa" Kanji variant vì có thể làm sai tên thật của khách hàng.
Romaji / Latin Characters
Tokyo
ABC
Product01
test@example.com
Người Nhật vẫn dùng Latin characters thường xuyên: mã sản phẩm (SKU), email, tên thương hiệu quốc tế, URL, mã nhân viên. Một input field như "Product Code" hay "Style No" thường chỉ nên cho phép Latin + số, không nên cho phép Kanji/Kana — ngược lại hoàn toàn so với field "Product Name".
Arabic Numbers Và Full-width Numbers
12345 (half-width, ASCII)
12345 (full-width)
Full-width number xuất hiện rất thường xuyên khi người dùng gõ tiếng Nhật, vì IME ở chế độ nhập toàn bộ full-width sẽ tự động biến số ASCII gõ vào thành full-width. Với các trường như số điện thoại, mã bưu điện, số lượng, giá tiền — hệ thống hầu như luôn cần convert về half-width number trước khi xử lý logic hoặc lưu database, vì các phép toán, so sánh số, và tích hợp API bên ngoài đều mong đợi ASCII digit.
Full-width vs Half-width: Phân Tích Chuyên Sâu
Đây là chủ đề quan trọng và dễ bị xử lý sai nhất trong toàn bộ bài viết.
ABC → ABC
123 → 123
abc → abc
ア → ア
Vì sao tồn tại cả hai? Full-width (zenkaku) và Half-width (hankaku) bắt nguồn từ thời kỳ máy tính Nhật Bản dùng font kích thước cố định: một ký tự Kanji/Kana cần chiều rộng gấp đôi ký tự Latin để hiển thị đẹp trên lưới monospace — đó là "full-width". Ngược lại, "half-width" tồn tại để tiết kiệm không gian hiển thị và tương thích với hệ thống cũ (terminal, POS, hệ thống bank/mainframe) vốn chỉ hỗ trợ character set 1-byte (JIS X 0201).
Vì sao người Nhật vẫn dùng cả hai ngày nay? Vì thói quen gõ: khi IME đang ở chế độ "toàn角" (zenkaku input mode) để gõ Kanji/Hiragana, người dùng gõ số hoặc chữ Latin xen kẽ (ví dụ địa chỉ "東京都渋谷区1-2-3") và IME có thể tự động convert số/chữ đó thành full-width mà người dùng không để ý. Đây chính là nguồn gốc phổ biến nhất của lỗi nhập nhầm: user không cố ý gõ full-width, mà do IME auto-convert.
Khi nào nên normalize (convert về canonical form)?
- Các trường có ý nghĩa số học hoặc logic (số điện thoại, mã bưu điện, số lượng, giá tiền, ngày tháng) — luôn nên convert full-width digit → half-width trước khi validate/lưu, vì không convert sẽ khiến
parseInt("123")cho kết quả sai hoặcNaN. - Các trường dùng để search/so khớp (mã sản phẩm, username, email) — nên normalize để tránh tình trạng hai user nhập cùng một mã nhưng ở hai dạng khác nhau (
ABC123vàABC123) bị coi là hai giá trị khác nhau. - Trường furigana yêu cầu Katakana — thường cần convert Hiragana ⇄ Katakana và Half-width Katakana → Full-width Katakana để đưa về một canonical form duy nhất trước khi lưu.
Khi nào KHÔNG nên tự động normalize?
- Các trường free text hiển thị nguyên văn (địa chỉ chi tiết, ghi chú, tên hiển thị) — nếu người dùng cố ý gõ
ABC(ví dụ để nhấn mạnh hoặc theo phong cách trình bày riêng), tự động convert vềABCcó thể làm sai lệch ý định hiển thị của họ, đặc biệt nếu dữ liệu này được in ra chứng từ pháp lý hoặc hiển thị lại y nguyên cho người dùng khác xem. - Nửa-rộng Katakana (
ア→ア) không nên tự động convert hai chiều một cách tuỳ tiện — quyết định "convert sang chiều nào" (luôn về full-width, hay giữ nguyên như legacy data) phải dựa trên business rule của hệ thống, không phải một mặc định kỹ thuật áp dụng chung cho mọi field.
Nguyên tắc cốt lõi: input số/mã có ý nghĩa logic → nên normalize; input hiển thị/tự do → cân nhắc kỹ trước khi normalize, và luôn phải có quyết định rõ ràng từ business, không phải Developer tự suy đoán.
Japanese Keyboard Và IME: Cách Người Nhật Nhập Text
Người Nhật dùng bàn phím JIS (có thêm phím chuyển đổi 半角/全角, ひらがな, カタカナ) hoặc bàn phím US kết hợp phần mềm IME (ví dụ Microsoft IME, Google Japanese Input, ATOK). Phổ biến nhất là gõ theo Romaji input: gõ chữ Latin theo cách phát âm, IME tự chuyển thành Hiragana, sau đó người dùng nhấn phím chuyển đổi (thường là Space) để IME đề xuất Kanji tương ứng.
ka → か
kyo → きょ
nihon → にほん → (nhấn Space) → 日本
Toàn bộ quá trình này diễn ra qua các bước:
Keyboard
↓
IME
↓
Composition (chuỗi ký tự đang "soạn thảo", chưa final)
↓
Candidate selection (chọn Kanji/Katakana phù hợp từ danh sách gợi ý)
↓
Committed text (ký tự đã chốt, ghi vào input field)
↓
Browser input event
Đây là lý do các sự kiện JavaScript sau không tương đương nhau, và việc chọn sai sự kiện là nguồn gốc phổ biến nhất của bug liên quan đến IME:
| Sự kiện | Ý nghĩa | Có nên dùng để validate Japanese input? |
|---|---|---|
keydown / keypress / keyup |
Bắt phím vật lý được nhấn | Không — trong lúc IME đang composition, các phím này phản ánh romaji trung gian (k, a), không phải ký tự cuối cùng |
compositionstart |
IME bắt đầu quá trình soạn thảo | Dùng để biết "đang trong composition", tạm dừng validate |
compositionupdate |
Nội dung composition thay đổi (candidate thay đổi) | Không nên validate/reject tại bước này |
compositionend |
IME đã chốt xong ký tự cuối cùng | Có — đây là thời điểm an toàn để chạy validation |
input |
Giá trị input đã thay đổi (bao gồm cả composition) | Cần kết hợp kiểm tra event.isComposing |
change |
Input mất focus và giá trị đã thay đổi | An toàn để validate lần cuối |
Nếu Developer viết logic kiểu "chặn ký tự không hợp lệ ngay khi gõ" bằng onKeyDown, hệ quả thực tế là: người dùng gõ k → bị chặn vì k không nằm trong whitespace Kanji cho phép — trong khi thực ra người dùng đang gõ dở kyo để ra きょ. Đây là lỗi UX nghiêm trọng và rất phổ biến ở các team chưa từng làm việc với thị trường Nhật.
Các Cách Người Dùng Nhập Dữ Liệu Thực Tế
- Gõ trực tiếp (nếu có bàn phím có sẵn Kanji hiếm — không phổ biến):
東京. - Gõ Romaji qua IME (phổ biến nhất):
toukyou→ (composition) →とうきょう→ (candidate selection) →東京. - Copy/Paste từ nguồn khác: người dùng có thể paste
12345,ABC,アイウ— những dạng full-width/half-width mà họ không tự gõ, mà lấy từ file/email/website khác. Paste luôn phải đi qua cùng một validation logic như khi gõ tay — một lỗi rất phổ biến là chỉ validate ở sự kiệnkeydown/keypressmà quên rằngpastekhông kích hoạt các sự kiện đó. - Paste từ Excel: có thể mang theo line break dạng
\r\n, số có định dạng ẩn (leading zero bị Excel tự xoá, ví dụ mã bưu điện0123456→123456), hoặc tab-separated values gây lẫn cột khi paste vào input field. - Paste từ Word: Word thường tự động chuyển đổi dấu ngoặc kép thẳng (
") thành dấu ngoặc kép cong kiểu "smart quotes" (" "), hoặc thay thế dấu gạch nối bằng em-dash — có thể gây ra ký tự lạ không mong muốn trong dữ liệu. - Paste từ website khác: có nguy cơ mang theo ký tự vô hình (zero-width space
U+200B, non-breaking spaceU+00A0) khiến chuỗi "trông giống hệt" một chuỗi khác nhưng validate hoặc so sánh lại thất bại. - Drag & Drop text từ file hoặc ứng dụng khác: hành vi tương tự paste, nhưng một số framework xử lý qua sự kiện khác (
drop) nên dễ bị bỏ sót nếu team chỉ test paste bằngCmd+V. - Mobile keyboard: bàn phím ảo trên iOS/Android có UX IME khác desktop — ví dụ predictive text có thể tự "sửa" một Kanji hiếm thành từ phổ biến hơn mà người dùng không để ý, và composition event trên mobile browser có thể không fire theo đúng thứ tự như desktop.
- Browser autocomplete/autofill: trình duyệt tự điền lại giá trị cũ đã lưu, có thể là dữ liệu ở format cũ (trước khi hệ thống áp dụng rule normalize mới) — dẫn đến dữ liệu "hợp lệ trong quá khứ" nhưng fail validation hiện tại.
Unicode, UTF-8 Và Text Tiếng Nhật
Một character (ký tự) mà con người nhìn thấy (A, あ, 漢, 😀) được máy tính lưu trữ dưới dạng một code point trong chuẩn Unicode (U+0041, U+3042, U+6F22, U+1F600), và code point đó lại được mã hoá (encode) thành các bytes cụ thể — phổ biến nhất là UTF-8, một encoding có độ dài thay đổi (variable-length):
| Ký tự | Loại | Số bytes trong UTF-8 |
|---|---|---|
A |
Latin (ASCII) | 1 byte |
あ |
Hiragana | 3 bytes |
漢 |
Kanji | 3 bytes |
😀 |
Emoji | 4 bytes |
Vì vậy, độ dài tính theo ký tự (character length) không nhất thiết bằng độ dài tính theo byte (byte length) — và mỗi ngôn ngữ lập trình xử lý điều này khác nhau:
- JavaScript/TypeScript:
"あ".lengthtrả về1vìString.lengthđếm theo UTF-16 code unit — nhưng với emoji nằm ngoài Basic Multilingual Plane (như"😀".length), kết quả là2do surrogate pair, dễ gây bug khi giới hạn số ký tự nhập vào bằng.lengthmột cách ngây thơ. - PHP:
strlen("あ")trả về3(đếm byte UTF-8), trong khimb_strlen("あ", "UTF-8")trả về1(đếm ký tự thực sự). Đây là một trong những bug phổ biến nhất khi Developer chưa quen làm việc với multi-byte string. - Laravel: các validation rule như
max:20trênstringsử dụngmb_strlen()nội bộ theo mặc định (kể từ khi framework hỗ trợ multi-byte string đúng cách), nhưng nếu Developer tự viết custom logic đếm độ dài bằngstrlen()thô, giới hạn 20 ký tự Kanji sẽ vô tình chỉ cho phép khoảng 6-7 ký tự thực tế (20 / 3). - MySQL: khai báo
VARCHAR(255)nghĩa là tối đa 255 characters, không phải 255 bytes — nhưng dung lượng storage/index thực tế được tính theo bytes (255 × 4bytes nếu dùngutf8mb4), điều này ảnh hưởng trực tiếp đến giới hạn độ dài index (Specified key was too longerror) khi cột tiếng Nhật tham giaUNIQUE KEYhoặc composite index.
Unicode Normalization: NFC, NFD, NFKC, NFKD
Unicode cho phép cùng một ký tự nhìn giống hệt nhau có nhiều cách biểu diễn code point khác nhau. Ví dụ kinh điển với tiếng Nhật: chữ が (ga — hiragana có dakuten) có thể là:
が → một code point duy nhất (U+304C, "precomposed")
か + ゙ → hai code point: か (U+304B) + dấu dakuten kết hợp (U+3099, "combining", decomposed)
Hai chuỗi này nhìn giống hệt nhau khi hiển thị, nhưng nếu so sánh bằng === trong code hoặc WHERE column = 'が' trong SQL mà không normalize trước, chúng có thể bị coi là khác nhau tuỳ vào cách dữ liệu được nhập vào (một số bàn phím/OS tạo ra dạng decomposed, một số tạo ra dạng precomposed).
Bốn dạng chuẩn hoá Unicode:
- NFC (Normalization Form Canonical Composition): ưu tiên dạng precomposed (một code point). Thường dùng làm canonical form mặc định để lưu database.
- NFD (Canonical Decomposition): tách thành base character + combining mark.
- NFKC (Compatibility Composition): giống NFC nhưng còn chuẩn hoá cả các biến thể "tương thích" — full-width → half-width, ligature → chuỗi ký tự gốc.
- NFKD (Compatibility Decomposition): tương tự NFKC nhưng ở dạng decomposed.
NFKC là công cụ phổ biến nhất để xử lý bài toán full-width/half-width vì nó tự động convert A1ア → A1ア-kiểu chuẩn hoá (tuỳ ngôn ngữ implement, nhưng ý tưởng chung là quy về dạng "tương đương về mặt compatibility"). Tuy nhiên, NFKC có thể làm mất thông tin có ý nghĩa:
① (circled digit one, U+2460) --NFKC--> 1
㍿ (ligature "kabushiki gaisha") --NFKC--> 株式会社
ガ (half-width ka + half-width dakuten) --NFKC--> ガ
Với số thứ tự hiển thị dạng ①②③, việc NFKC biến nó thành 123 có thể làm mất ý nghĩa trình bày (ký hiệu danh sách) nếu áp dụng nhầm lên nội dung hiển thị thay vì chỉ áp dụng cho mục đích search/index. Nguyên tắc: dùng normalization cho tầng search/so sánh/lưu trữ canonical key, nhưng cân nhắc kỹ trước khi áp dụng lên nội dung hiển thị nguyên văn cho người dùng.
Ký Tự Đặc Thù Tiếng Nhật Dễ Bị Bỏ Sót
Đây là nhóm ký tự mà Developer/QC chưa quen với tiếng Nhật thường quên kiểm tra, nhưng lại là nguồn gốc của rất nhiều bug âm thầm:
- Dakuten
゛và Handakuten゜: dấu phụ biến âm, ví dụは(ha) →ば(ba, dakuten) →ぱ(pa, handakuten). Về mặt business, đây là ba ký tự/từ hoàn toàn khác nghĩa, không phải biến thể của nhau. - Combining characters: dạng decomposed của dakuten/handakuten (xem phần Normalization ở trên).
- Trường âm dài (chōonpu)
ー: dùng trong Katakana để kéo dài âm, ví dụコーヒー(coffee). Dễ bị nhầm với dấu gạch ngang-hoặc dấu gạch dưới khi OCR/copy từ nguồn khác. - Kana nhỏ:
ぁぃぅぇぉ(small a/i/u/e/o),ゃゅょ(small ya/yu/yo, dùng để tạo âm ghép nhưきょ= kyo), và 小さいつっ(small tsu, dùng để biểu thị âm bật/gemination, ví dụきって= kitte "tem thư" khác hẳnきて= kite "hãy đến"). - Iteration marks:
ゝゞ々(lặp lại ký tự trước, ví dụ人々= "mọi người",々lặp lại 人). - Japanese punctuation và brackets:
()「」『』【】、。・— xem phần riêng bên dưới. - Full-width symbols, circled numbers (
①②③), Roman numerals (Ⅰ Ⅱ Ⅲ): thường xuất hiện trong văn bản hành chính, hợp đồng, hoặc copy-paste từ PDF/Word. - Emoji: yêu cầu
utf8mb4(4 bytes) ở tầng database, nếu không sẽ gây lỗi insert với charsetutf8/utf8mb3cũ.
Hệ quả thực tế nếu bỏ sót nhóm ký tự này:
ソ (so) vs ゾ (zo, có dakuten) → hai category nghiệp vụ khác nhau, nhưng có thể bị collation coi là TRÙNG NHAU
は vs ば vs ぱ → ba giá trị khác nghĩa, nguy cơ bị so sánh nhầm là bằng nhau
つ vs っ → lỗi chính tả phổ biến khi gõ nhanh, có thể gây duplicate record nếu validate quá lỏng lẻo
や vs ゃ → tương tự, âm ghép vs âm đơn khác nghĩa hoàn toàn
Trường hợp ソ/ゾ ở trên không phải ví dụ lý thuyết — đây chính là nguyên nhân gốc của một sự cố có thật: một hệ thống MySQL dùng collation utf8mb4_unicode_ci đã âm thầm coi hai category nghiệp vụ khác nhau (ソース類 — nhóm "Sauce", và ゾーン雑貨 — nhóm "Zone goods") là giống nhau, dẫn đến việc một trong hai bị mất khỏi hệ thống khi chạy GROUP BY/de-dup. Phần Database bên dưới sẽ phân tích chi tiết case study này.
Japanese Punctuation
| Full-width (Nhật) | ASCII tương ứng | Ghi chú |
|---|---|---|
() |
() |
Ngoặc đơn |
「」 |
"" hoặc '' |
Ngoặc trích dẫn cấp 1 |
『』 |
— | Ngoặc trích dẫn cấp 2 (trích trong trích) |
【】 |
[] |
Ngoặc nhấn mạnh tiêu đề |
、 |
, |
Dấu phẩy Nhật |
。 |
. |
Dấu chấm câu Nhật |
・ |
— | Dấu chấm giữa (phân tách từ mượn, danh sách) |
: ; ! ? |
: ; ! ? |
Full-width của các dấu câu ASCII |
Người dùng Nhật luôn có xu hướng gõ dấu câu full-width khi đang ở chế độ IME tiếng Nhật, vì bàn phím sẽ tự chuyển dấu phẩy/chấm ASCII thành 、/。. Với các trường free text (ghi chú, mô tả sản phẩm, địa chỉ), nên cho phép cả hai loại dấu câu. Với các trường có ý nghĩa cấu trúc dữ liệu (ví dụ giá trị JSON, mã code, URL), nên reject full-width punctuation vì chúng không có ý nghĩa hợp lệ trong ngữ cảnh đó. Việc normalize dấu câu (convert 、。 → ,.) chỉ nên áp dụng khi có yêu cầu rõ ràng, ví dụ khi xuất dữ liệu sang hệ thống quốc tế không hỗ trợ ký tự full-width.
Thiết Kế Validation: Decision Framework
Thay vì trả lời "dùng regex nào", câu hỏi đúng cần đặt ra với mỗi loại input là: cho phép ký tự gì, có cần normalize không, và validate ở tầng nào. Dưới đây là khung quyết định cho các loại input phổ biến nhất trong hệ thống Nhật Bản:
| Input Type | Hiragana | Katakana | Kanji | Latin | Full-width số | Half-width số | Normalize |
|---|---|---|---|---|---|---|---|
| User Name (氏名) | ✅ | ✅ | ✅ | ✅ (tên nước ngoài) | ⚠️ tuỳ | ⚠️ tuỳ | Không tự ý convert, giữ nguyên như user nhập |
| Furigana (フリガナ) | ⚠️ tuỳ hệ thống | ✅ (thường bắt buộc) | ❌ | ❌ | ❌ | ❌ | Convert Half-width Katakana → Full-width Katakana |
| Product Name | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Không normalize (hiển thị nguyên văn) |
| ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | Convert full-width → half-width trước khi validate format email | |
| Phone Number | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | Bắt buộc convert full-width digit → half-width |
| Postal Code | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | Bắt buộc convert, giữ leading zero |
| Address (địa chỉ) | ✅ | ✅ | ✅ | ✅ | ⚠️ tuỳ | ⚠️ tuỳ | Số nhà nên convert về half-width; tên đường/khu vực giữ nguyên |
| SKU / Product Code | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | Bắt buộc convert, reject ký tự ngoài [A-Z0-9-] |
| Style No | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | Tương tự SKU |
| Search Keyword | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Normalize ở tầng search index (không sửa input gốc của user) |
| Free Text (ghi chú) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Không normalize, giữ nguyên nội dung người dùng nhập |
Với mỗi field, 15 câu hỏi sau nên được trả lời rõ ràng trong tài liệu requirement, không để Developer tự suy đoán: ký tự nào được phép (Hiragana/Katakana/Kanji/Latin/số) và bị cấm; có cho phép full-width, có cho phép half-width; có normalize hay convert; validation message hiển thị nội dung gì; validate ở frontend, backend, hay cả hai; và database có cần constraint (CHECK, charset, collation) để enforce rule đó ở tầng thấp nhất hay không.
Allow vs Reject vs Normalize vs Convert: Decision Tree
Bốn chiến lược xử lý input cần được phân biệt rõ ràng:
- Allow: Giữ nguyên input, không sửa đổi, không chặn. Dùng cho các trường tự do (free text, product name, tên riêng).
- Reject: Từ chối input nếu chứa ký tự không hợp lệ, yêu cầu người dùng sửa lại. Dùng cho các trường có cấu trúc chặt (email, SKU, mã bưu điện).
- Normalize: Chuyển đổi representation về một dạng canonical duy nhất mà không thay đổi loại ký tự — ví dụ
が(decomposed) →が(precomposed NFC), hoặcパ(half-width ka+handakuten) →パ(full-width, một code point). - Convert: Chuyển đổi loại ký tự theo business rule cụ thể — ví dụ full-width digit → half-width digit, Hiragana → Katakana cho trường furigana.
Cần convert full-width ⇄ half-width không?
│
├─ Có ý nghĩa số học/logic (số điện thoại, giá, số lượng)?
│ └─→ CONVERT bắt buộc về half-width
│
├─ Là mã định danh dùng để search/so khớp (SKU, email, username)?
│ └─→ NORMALIZE + CONVERT về canonical form (thường half-width)
│
├─ Là nội dung hiển thị tự do, không dùng để so sánh logic (địa chỉ chi tiết, ghi chú)?
│ └─→ ALLOW nguyên trạng, không tự ý convert
│
└─ Business đã có yêu cầu rõ ràng khác?
└─→ Tuân theo yêu cầu đó, document lại quyết định
Ví dụ kinh điển cần phân biệt:
123 → 123 (nên convert — có ý nghĩa số học)
ABC → ABC (nên convert nếu là mã/code; giữ nguyên nếu là tên hiển thị theo phong cách riêng)
ア → ア (KHÔNG nên tự động convert hai chiều nếu chưa có business rule rõ ràng — quyết định chiều convert, hoặc không convert, phải đến từ yêu cầu nghiệp vụ)
Input UX: 3 Chiến Lược
A. Chặn ngay khi gõ (onKeyDown filtering): chặn phím không hợp lệ ngay tại thời điểm nhấn phím. UX cảm giác "phản hồi tức thì", nhưng phá vỡ hoàn toàn IME composition vì trong lúc gõ romaji trung gian, phím vật lý không phản ánh ký tự cuối cùng. Đây là chiến lược không nên dùng cho bất kỳ trường nào có thể chứa tiếng Nhật.
B. Cho phép nhập, validate sau (khuyến nghị): người dùng gõ tự do, validation chạy tại compositionend, blur, hoặc submit. Tương thích hoàn toàn với IME, mobile keyboard, paste, autofill. Nhược điểm duy nhất là người dùng thấy lỗi hơi trễ hơn (sau khi gõ xong), nhưng đây là đánh đổi hợp lý và là chuẩn UX phổ biến nhất cho input tiếng Nhật.
C. Cho phép nhập và normalize tự động: kết hợp chiến lược B với việc tự động convert (ví dụ full-width digit → half-width) ngay sau khi người dùng gõ xong, hiển thị lại giá trị đã chuẩn hoá. Phù hợp cho các trường số/mã, nhưng cần thông báo rõ cho người dùng biết giá trị đã được tự động điều chỉnh (tránh gây bối rối "tôi gõ khác mà sao hiển thị khác").
| Tiêu chí | A. Block on keydown | B. Validate after | C. Validate + Normalize |
|---|---|---|---|
| Tương thích IME | ❌ Rất kém | ✅ Tốt | ✅ Tốt |
| Tương thích Mobile | ❌ Kém | ✅ Tốt | ✅ Tốt |
| UX phản hồi | Tức thì nhưng gây khó chịu khi gõ tiếng Nhật | Chậm hơn một nhịp, nhưng tự nhiên | Chậm hơn một nhịp, có thể gây bất ngờ nếu không thông báo |
| Độ phức tạp code | Thấp nhưng sai về bản chất | Trung bình | Cao hơn (cần xử lý display vs canonical value) |
| Rủi ro | Chặn nhầm input hợp lệ | Thấp | Có thể gây nhầm lẫn nếu convert không rõ ràng với user |
Validation Message
Message không nên chỉ nói "Invalid input" — với người dùng Nhật, message cần nêu rõ loại ký tự được yêu cầu:
半角英数字で入力してください。 (Vui lòng nhập bằng chữ/số half-width)
全角カタカナで入力してください。 (Vui lòng nhập bằng Katakana full-width)
半角数字で入力してください。 (Vui lòng nhập bằng số half-width)
ひらがなで入力してください。 (Vui lòng nhập bằng Hiragana)
| Ngữ cảnh | Tiếng Nhật | Tiếng Anh |
|---|---|---|
| Error message (SKU) | 半角英数字とハイフンのみ使用できます。 |
"Only half-width letters, numbers, and hyphens are allowed." |
| Placeholder (Furigana) | 例:タナカ タロウ |
"e.g. TANAKA TARO" |
| Helper text (Postal code) | 半角数字7桁で入力してください(例:123-4567) |
"Enter 7 half-width digits (e.g. 123-4567)" |
Frontend Implementation
Với React/TypeScript, validate Japanese input đúng cách nghĩa là tách biệt composition state khỏi validation logic:
function KatakanaInput() {
const [value, setValue] = useState('');
const [isComposing, setIsComposing] = useState(false);
const [error, setError] = useState('');
// Chỉ cho phép Full-width Katakana + khoảng trắng
const KATAKANA_PATTERN = /^[゠-ヿ \s]*$/;
const validate = (text: string) => {
if (!KATAKANA_PATTERN.test(text)) {
setError('全角カタカナで入力してください。');
} else {
setError('');
}
};
return (
<input
inputMode="text"
pattern="[゠-ヿ]*"
maxLength={50}
value={value}
onChange={(e) => {
setValue(e.target.value);
// Bỏ qua validate khi đang trong composition (IME đang soạn thảo)
if (!isComposing) validate(e.target.value);
}}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={(e) => {
setIsComposing(false);
// Chỉ validate SAU KHI IME đã chốt xong ký tự cuối cùng
validate((e.target as HTMLInputElement).value);
}}
/>
);
}
Giải thích:
onCompositionStart/onCompositionEnd: đánh dấu ranh giới bắt đầu/kết thúc quá trình IME soạn thảo — đây là hai sự kiện quan trọng nhất để tránh validate nhầm dữ liệu trung gian.onChangevẫn được set state bình thường (để input hiển thị mượt mà theo từng ký tự), nhưng logic validate/hiển thị lỗi chỉ chạy khi!isComposing.KATAKANA_PATTERNdùng Unicode range゠-ヿ(khối Full-width Katakana) thay vì cố liệt kê từng ký tự — cách viết chuẩn và dễ maintain hơn.maxLengthlà giới hạn ở tầng DOM (đếm theo UTF-16 code unit, không hoàn toàn chính xác với ký tự ngoài BMP như một số emoji hiếm, nhưng đủ dùng cho Hiragana/Katakana/Kanji thông thường).- Không dùng
onKeyDownđể chặn phím — đây chính là nguyên nhân phổ biến nhất phá vỡ trải nghiệm gõ tiếng Nhật.
Backend Implementation
Với PHP/Laravel, lỗi phổ biến nhất là dùng strlen() thay vì mb_strlen():
// SAI: strlen() đếm byte, không đếm ký tự
if (strlen($request->name) > 20) {
return response()->json(['error' => 'Tên quá dài'], 422);
}
// "田中太郎" (4 ký tự Kanji) = 12 bytes trong UTF-8 → bị reject nhầm dù chỉ 4 ký tự!
// ĐÚNG: dùng mb_strlen() để đếm đúng số ký tự
if (mb_strlen($request->name, 'UTF-8') > 20) {
return response()->json(['error' => 'Tên quá dài'], 422);
}
Với Laravel Form Request, kết hợp Rule custom cho Full-width Katakana và convert full-width số:
class UpdateCustomerRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'], // Laravel 'max' dùng mb_strlen nội bộ
'name_kana' => ['required', 'string', 'regex:/^[\x{30A0}-\x{30FF}\x{3000}\s]+$/u'],
'postal_code' => ['required', 'regex:/^[0-9]{3}-?[0-9]{4}$/'],
];
}
protected function prepareForValidation(): void
{
$this->merge([
// Convert full-width digit → half-width TRƯỚC KHI validate
'postal_code' => mb_convert_kana($this->postal_code, 'n'),
// Convert half-width Katakana → full-width Katakana cho trường furigana
'name_kana' => mb_convert_kana($this->name_kana, 'KV'),
]);
}
}
Giải thích:
mb_convert_kana($str, 'n'): convert full-width digit → half-width digit (n= numbers).mb_convert_kana($str, 'KV'): convert half-width Katakana (K) sang full-width Katakana (V= "to zenkaku"), cách viết phổ biến để chuẩn hoá trường furigana.prepareForValidation()chạy trước rule validation, đảm bảo dữ liệu đã được normalize trước khi kiểm tra pattern — tránh tình trạng dữ liệu hợp lệ về mặt ý nghĩa nhưng bị reject chỉ vì sai representation (full-width vs half-width).
Database: Charset, Collation Và Các Vấn Đề Với MySQL/MariaDB
Ở tầng database, hai khái niệm cốt lõi cần nắm là character set (database có thể lưu được ký tự nào) và collation (database so sánh/sắp xếp các ký tự đó như thế nào). Với hệ thống tiếng Nhật, utf8mb4 là bắt buộc (charset utf8/utf8mb3 cũ trong MySQL chỉ hỗ trợ tối đa 3 bytes/ký tự, không lưu được emoji và một số ký tự CJK hiếm).
Vấn đề tinh vi và nguy hiểm hơn nằm ở collation: utf8mb4_general_ci và utf8mb4_unicode_ci cho ra kết quả so sánh khác nhau với chính các ký tự đặc thù tiếng Nhật đã nêu ở phần trên:
| Cặp ký tự | general_ci |
unicode_ci |
|---|---|---|
ソ vs ゾ (dakuten) |
Khác nhau | Coi là bằng nhau |
は vs ば (dakuten) |
Khác nhau | Coi là bằng nhau |
は vs ぱ (handakuten) |
Khác nhau | Coi là bằng nhau |
あ vs ぁ (kana thường/nhỏ) |
Khác nhau | Coi là bằng nhau |
Đây chính xác là nguyên nhân của một sự cố có thật từng xảy ra trên một hệ thống legacy Nhật Bản: hai category nghiệp vụ hoàn toàn khác nhau — ソース類 ("Sauce") và ゾーン雑貨 ("Zone goods") — chỉ khác nhau ở dấu dakuten trên ký tự đầu, đã bị unicode_ci coi là trùng lặp khi chạy GROUP BY, khiến một trong hai category bị mất khỏi hệ thống mà không hề có exception nào được ném ra — dữ liệu vẫn chạy trơn tru, chỉ là sai về mặt nghiệp vụ.
-- Với general_ci: 2 category được đếm đúng, tách biệt
SELECT category_name, COUNT(*) FROM category_group
GROUP BY category_name COLLATE utf8mb4_general_ci;
-- ソース類 | 1
-- ゾーン雑貨 | 1
-- Với unicode_ci: 2 category bị GỘP LÀM MỘT một cách âm thầm
SELECT category_name, COUNT(*) FROM category_group
GROUP BY category_name COLLATE utf8mb4_unicode_ci;
-- ソース類 / ゾーン雑貨 | 2 ← không có lỗi, nhưng dữ liệu đã sai
Nguyên tắc rút ra: với hệ thống migrate từ legacy MySQL cũ (vốn mặc định utf8_general_ci từ thời chưa có utf8mb4), không nên mặc định đổi sang unicode_ci chỉ vì đó là "default hiện đại" — phải audit dữ liệu thật, test cụ thể với các cặp ký tự dakuten/handakuten, và quyết định collation dựa trên hành vi so sánh mà business thực sự cần, không phải dựa trên "cái nào mới hơn". Collation ảnh hưởng trực tiếp đến =, LIKE, JOIN, GROUP BY, ORDER BY, và UNIQUE KEY — một lựa chọn sai có thể gây duplicate detection sai, JOIN mismatch, hoặc mất dữ liệu hợp lệ trong quá trình de-dup. Phần lý thuyết đầy đủ về charset/collation, cách audit ở cấp column, và quy trình migrate an toàn được trình bày chi tiết trong bài "Character Set và Collation trong MySQL" của course Database — nên đọc kèm bài này nếu bạn cần đi sâu vào tầng database.
API / JSON / CSV / Excel: Mojibake Và Encoding Debug
Khi text tiếng Nhật đi qua chuỗi Frontend → JSON → API → Backend → Database rồi Database → CSV → Excel, một loại lỗi đặc trưng có thể xuất hiện: 文字化け (mojibake) — text hiển thị thành ký tự lạ, vô nghĩa (ví dụ 譁�蟄怜喧縺� thay vì 文字化け), do dữ liệu bị decode sai encoding ở đâu đó trong chuỗi xử lý.
Các nguyên nhân phổ biến nhất:
- Thiếu UTF-8 BOM khi export CSV cho Excel: Excel trên Windows, khi mở file CSV không có BOM (
Byte Order Mark, 3 byteEF BB BFở đầu file), sẽ tự đoán encoding là Shift_JIS hoặc ANSI theo locale hệ thống thay vì UTF-8, khiến toàn bộ tiếng Nhật hiển thị thành mojibake dù dữ liệu gốc hoàn toàn đúng. Giải pháp: luôn thêm BOM khi generate CSV dành cho người dùng mở bằng Excel (\xEF\xBB\xBF+ nội dung CSV UTF-8). - Legacy system dùng Shift_JIS/CP932: nhiều hệ thống Nhật Bản cũ lưu trữ bằng Shift_JIS thay vì UTF-8. Khi migrate hoặc tích hợp, nếu application decode nhầm Shift_JIS như UTF-8 (hoặc ngược lại), dữ liệu bị hỏng ngay từ bước đọc file — không có collation hay charset nào ở tầng database có thể cứu lại dữ liệu đã bị decode sai từ trước.
- Line break không nhất quán: Excel trên Windows dùng
\r\n, trong khi nhiều backend xử lý mặc định theo\n— có thể gây lỗi parse CSV nếu không xử lý đúng. - Dấu phẩy và dấu ngoặc kép trong nội dung: một địa chỉ tiếng Nhật có thể chứa dấu phẩy full-width
、(an toàn, không xung đột với CSV delimiter) nhưng nếu dữ liệu vô tình chứa dấu phẩy ASCII,chưa được quote đúng chuẩn CSV (RFC 4180), file sẽ bị lệch cột khi mở bằng Excel.
Cách debug mojibake nhanh: kiểm tra bytes thô của chuỗi lỗi — nếu là do UTF-8 bị decode nhầm thành Latin-1/Windows-1252 rồi encode lại thành UTF-8 ("double-encoding"), có thể phục hồi bằng cách decode ngược lại đúng chuỗi encoding đã áp dụng sai; nếu là do thiếu BOM, việc thêm BOM vào file gốc và mở lại thường giải quyết ngay lập tức.
Search, Sort, So Sánh (Compare) Text Tiếng Nhật
Ba câu hỏi cốt lõi khi thiết kế search/sort cho text tiếng Nhật: ký tự nào được coi là "giống nhau", ai quyết định điều đó, và quyết định đó được enforce ở tầng nào.
ア (full-width katakana)
ア (half-width katakana)
あ (hiragana)
Ba ký tự trên đều phát âm là "a", nhưng về mặt Unicode là ba code point hoàn toàn khác nhau. Một search engine "thông minh" có thể coi cả ba là tương đương (search "ア" ra luôn cả "あ" và "ア"), nhưng đây phải là một quyết định business rõ ràng (ví dụ: "cho phép tìm sản phẩm bằng cả Hiragana lẫn Katakana vì khách hàng có thể gõ theo thói quen khác nhau"), không phải hành vi ngẫu nhiên do collation mặc định của database gây ra.
Tương tự với ソ vs ゾ — nếu business coi đây là hai giá trị khác nhau (đúng theo ý nghĩa ngôn ngữ học), thì collation unicode_ci (coi là giống nhau) sẽ gây lỗi nghiệp vụ; nếu business chủ động muốn "tìm mờ" (fuzzy search) bỏ qua dakuten, thì đó lại là hành vi mong muốn — miễn là được thiết kế có chủ đích ở tầng search (ví dụ qua full-text search engine với rule riêng), thay vì bị quyết định ngẫu nhiên bởi default collation của database dùng cho toàn bộ exact-match logic khác (UNIQUE, JOIN).
Nguyên tắc: tách biệt "database default collation" (dùng cho exact match, UNIQUE, JOIN) khỏi "search behavior" (có thể cần fuzzy hơn, dùng full-text search hoặc search engine riêng như Elasticsearch với custom analyzer cho tiếng Nhật). Không nên dùng chung một collation cho cả hai mục đích.
Catalog Các Bug Thường Gặp
Bug 1 — Validation chạy giữa quá trình IME composition
- Problem: Input Katakana bị báo lỗi ngay khi người dùng đang gõ dở romaji.
- Cause: Validate tại
onKeyDown/onInputmà không kiểm traisComposing. - Repro: Gõ "ka" trong ô chỉ cho phép Katakana — thấy lỗi hiện ra ngay khi gõ
k. - Expected: Không hiện lỗi cho đến khi IME chốt xong ký tự.
- Fix: Bỏ qua validate khi
event.isComposing === true, chỉ validate tạicompositionend/blur. - Test case: Gõ Katakana bằng IME thật (không paste), xác nhận không có lỗi nhấp nháy giữa chừng.
Bug 2 — strlen() thay vì mb_strlen()
- Problem: Tên 10 ký tự Kanji bị reject vì "quá dài" dù giới hạn là 20 ký tự.
- Cause: Backend dùng
strlen()đếm byte (10 Kanji = 30 bytes) thay vìmb_strlen(). - Fix: Thay toàn bộ
strlen()liên quan đến user-facing string bằngmb_strlen($str, 'UTF-8'). - Test case: Nhập chính xác N ký tự Kanji (không hơn không kém), xác nhận pass đúng ở giới hạn N.
Bug 3 — Mojibake khi export CSV mở bằng Excel
- Problem: File CSV export ra chứa toàn ký tự lạ khi mở bằng Excel trên Windows.
- Cause: Thiếu UTF-8 BOM, Excel tự đoán sai encoding.
- Fix: Thêm BOM (
\xEF\xBB\xBF) vào đầu file CSV trước khi trả về response. - Test case: Export CSV chứa tên Kanji/Katakana, mở trực tiếp bằng Excel (không qua text editor), xác nhận hiển thị đúng.
Bug 4 — Duplicate dữ liệu do collation unicode_ci
- Problem: Hai category nghiệp vụ khác nhau (
ソvsゾ) bị coi là trùng, một category biến mất sau khi de-dup. - Cause: Column dùng
utf8mb4_unicode_ci, coi cặp ký tự dakuten là bằng nhau. - Fix: Đổi collation về
utf8mb4_general_ci(nếu đây là hành vi legacy cần giữ) hoặc audit lại toàn bộ business rule về so sánh trước khi chọn collation cho hệ thống mới. - Test case: Insert hai giá trị chỉ khác nhau ở dakuten, chạy
SELECT DISTINCT/GROUP BY, xác nhận kết quả trả về đúng 2 dòng riêng biệt (nếu business yêu cầu phân biệt).
Bug 5 — Full-width/Half-width mismatch khi search
- Problem: Khách hàng tìm sản phẩm bằng mã
ABC123nhưng hệ thống lưuABC123, không ra kết quả. - Cause: Không normalize input tìm kiếm và dữ liệu lưu về cùng một canonical form.
- Fix: Normalize cả input search và dữ liệu lưu trữ (hoặc index) về half-width trước khi so sánh.
- Test case: Lưu sản phẩm với mã half-width, search bằng full-width (và ngược lại), xác nhận đều ra kết quả.
Bug 6 — Paste bypass validation
- Problem: Người dùng paste
123(full-width) vào ô chỉ nhận half-width digit, dữ liệu vẫn được lưu. - Cause: Validation chỉ gắn ở sự kiện bàn phím, không áp dụng cho
paste. - Fix: Validate tại
onChange/onBlur(bắt được cả giá trị sau khi paste), không chỉ tại sự kiện phím. - Test case: Copy giá trị full-width từ nguồn khác, paste vào field, xác nhận validation vẫn kích hoạt đúng.
QA/QC Comprehensive Checklist
Keyboard
- [ ] Japanese JIS keyboard
- [ ] US keyboard + Japanese IME
- [ ] IME ON / IME OFF
- [ ] Hiragana input mode / Katakana input mode / Latin input mode
Input Content
- [ ] Hiragana / Katakana / Half-width Katakana / Kanji
- [ ] Latin / Full-width Latin / Half-width Latin
- [ ] Full-width số / Half-width số
- [ ] Japanese punctuation / ASCII punctuation
- [ ] Symbols / Emoji / Circled numbers / Roman numerals
- [ ] Dakuten/Handakuten pairs (
は/ば/ぱ,ソ/ゾ), small kana (つ/っ,や/ゃ)
Interaction
- [ ] Gõ trực tiếp qua IME (không chỉ paste sẵn dữ liệu test)
- [ ] Copy / Paste / Cut
- [ ] Drag & Drop
- [ ] Browser autofill / autocomplete
- [ ] Mobile keyboard (iOS + Android, cả hai)
Validation
- [ ] Validate khi gõ tay
- [ ] Validate khi paste
- [ ] Không hiện lỗi giữa lúc IME đang composition
- [ ] Empty value / Maximum length (đếm đúng theo ký tự) / Minimum length
- [ ] Ký tự không hợp lệ bị reject đúng, ký tự hợp lệ không bị reject nhầm
- [ ] Mixed input (ví dụ tên vừa Kanji vừa Latin) xử lý đúng theo rule field
Backend
- [ ] API validation dùng đúng độ dài ký tự (
mb_strlen, không phảistrlen) - [ ] Normalization (full-width/half-width, Hiragana/Katakana) áp dụng đúng trước khi lưu
- [ ] Business rule allow/reject/normalize/convert được implement đúng theo tài liệu, không tự suy đoán
Database
- [ ] Charset là
utf8mb4cho toàn bộ bảng/cột liên quan - [ ] Collation được chọn có chủ đích, đã test với cặp ký tự dakuten/handakuten thật
- [ ] Search / Sort /
JOIN/UNIQUEcho kết quả đúng với dữ liệu tiếng Nhật thật (không chỉ test bằngABC/abc)
Export / Import
- [ ] CSV có BOM khi dành cho Excel
- [ ] Import CSV không làm mất leading zero (mã bưu điện, mã sản phẩm)
- [ ] Excel mở file export ra không bị mojibake
- [ ] Import dữ liệu full-width/half-width lẫn lộn vẫn được normalize đúng khi lưu
Test Matrix
| Category | Input mẫu | Expected | Ghi chú |
|---|---|---|---|
| Hiragana | あいう |
Allow (hầu hết field tên/free text) | Không normalize |
| Katakana | アイウ |
Allow | Không normalize |
| Half-width Katakana | アイウ |
Tuỳ business: convert sang full-width, hoặc reject nếu field yêu cầu nghiêm ngặt | Không có rule mặc định chung |
| Kanji | 日本 |
Allow (trừ các field mã/code) | Không normalize |
| Full-width số | 123 |
Tuỳ business: convert về half-width nếu field có ý nghĩa số học; allow nguyên trạng nếu là free text | Quyết định theo field |
| Half-width số | 123 |
Allow | Canonical form cho số |
| Full-width Latin | ABC |
Tuỳ business: convert nếu là mã/code; allow nếu là tên hiển thị | Quyết định theo field |
| Half-width Latin | ABC |
Allow | Canonical form cho mã/code |
| Dakuten pair | ソ vs ゾ |
Phải được coi là KHÁC NHAU trừ khi business yêu cầu fuzzy search | Phụ thuộc collation database |
Lưu ý quan trọng: bảng trên không khẳng định một hành vi "đúng" tuyệt đối cho mọi hệ thống — expected behavior luôn do business requirement quyết định, bảng chỉ giúp đặt đúng câu hỏi cho từng loại input.
Coding Guidelines: Japanese Text Development Standard
- Không bao giờ giả định một ký tự tiếng Nhật chiếm 1 byte.
- Không dùng
strlen()để tính độ dài ký tự tiếng Nhật — luôn dùngmb_strlen()hoặc tương đương. - Không validate/filter Japanese IME input chỉ bằng
keydown/keypress— luôn xử lý composition event. - Luôn test hành vi paste, không chỉ test gõ tay.
- Với mỗi input field, định nghĩa rõ có cần convert Full-width ⇄ Half-width hay không, và theo chiều nào.
- Định nghĩa rõ ràng normalization rule (NFC/NFKC, Hiragana⇄Katakana) trước khi code, đừng để Developer tự quyết định ngầm.
- Tách biệt rõ validation ở frontend (UX, phản hồi nhanh) và validation ở backend (nguồn sự thật, bắt buộc).
- Định nghĩa rõ ràng charset/collation ở tầng database, đừng dùng mặc định của framework mà không kiểm tra.
- Không tự ý biến đổi (convert/normalize) dữ liệu người dùng nhập mà chưa có sự đồng ý của business.
- Luôn test Japanese input trên cả desktop lẫn mobile, với cả JIS keyboard/IME thật, không chỉ hardcode chuỗi test có sẵn.
- Luôn test riêng các cặp ký tự dakuten/handakuten (
は/ば/ぱ,ソ/ゾ) khi làm việc vớiUNIQUE,JOIN,GROUP BY. - Luôn thêm UTF-8 BOM khi export CSV cho người dùng mở bằng Excel.
- Không dùng chung một collation cho cả "exact match nghiệp vụ" (UNIQUE, JOIN) và "fuzzy search" — tách riêng hai mục đích này.
- Khi migrate từ hệ thống legacy, audit charset/collation thật của dữ liệu gốc thay vì mặc định dùng cấu hình "hiện đại" của framework mới.
- Ghi lại (document) mọi quyết định về allow/reject/normalize/convert cho từng field — đây là business rule, không phải chi tiết implementation nội bộ.
Requirement Q&A Checklist
Dùng checklist này khi nhận một requirement input field mới liên quan đến tiếng Nhật:
Input field:
- Cho phép Hiragana? Katakana? Kanji? Latin? Số?
- Cho phép Full-width? Half-width?
- Cho phép ký tự đặc biệt/dấu câu nào?
Normalization:
- Full-width → Half-width có bắt buộc không?
- Half-width Katakana → Full-width Katakana có bắt buộc không (trường furigana)?
- Có cần áp dụng Unicode normalization (NFC/NFKC) không?
Validation:
- Chiến lược là Allow, Reject, Normalize, hay Convert?
- Validate ở đâu: frontend, backend, cả hai?
- Message lỗi hiển thị nội dung gì (tiếng Nhật, tiếng Anh)?
Storage:
- Charset của cột: utf8mb4?
- Collation của cột: general_ci hay unicode_ci — đã test với dữ liệu dakuten thật chưa?
- Có tham gia UNIQUE/JOIN không? Đã audit rủi ro trùng lặp do collation chưa?
Testing:
- Đã test bằng IME thật (không chỉ paste chuỗi có sẵn) chưa?
- Đã test copy/paste, mobile keyboard, autofill chưa?
- Đã test export/import CSV/Excel với dữ liệu tiếng Nhật thật chưa?
Phân Biệt Business Rule Và Technical Rule
Không có một quy tắc kỹ thuật chung đúng cho mọi hệ thống rằng "input tiếng Nhật phải luôn Full-width" hay "phải luôn Half-width". Mọi quyết định về ký tự/normalize/convert phải đi theo chuỗi:
Business Requirement
↓
Input Rule (ký tự nào được phép)
↓
Validation Rule (allow/reject ở đâu, message gì)
↓
Normalization Rule (có convert không, theo chiều nào)
↓
Storage Rule (charset, collation)
↓
Search / Sort Rule (exact match hay fuzzy match)
Developer không nên tự quyết định bất kỳ bước nào trong chuỗi này dựa trên "thói quen kỹ thuật" — mỗi quyết định phải bắt nguồn từ business requirement rõ ràng, và được document lại để tránh tình trạng mỗi Developer tự xử lý một kiểu trên cùng một hệ thống.
Ví Dụ Thực Tế: Legacy System Migration
Các hệ thống web thực tế tại Nhật Bản thường gặp các trường sau, mỗi trường có rule khác nhau: tên khách hàng/nhân viên (氏名) cho phép mọi loại ký tự, không normalize, giữ nguyên như user nhập vì đây là tên pháp lý; furigana hầu như luôn yêu cầu Full-width Katakana, convert bắt buộc từ Hiragana hoặc Half-width Katakana; mã bưu điện (7 chữ số, có gạch ngang) bắt buộc convert full-width → half-width và giữ leading zero; địa chỉ cho phép mọi loại ký tự ở phần tên đường/khu vực, nhưng số nhà nên convert về half-width; mã sản phẩm/SKU/Style No chỉ cho phép Latin + số half-width, reject nghiêm ngặt các loại khác; CSV import/export giữa hệ thống cũ (thường Shift_JIS) và hệ thống mới (UTF-8) cần một bước convert encoding tường minh, không thể giả định cả hai phía dùng cùng encoding.
Trong các dự án legacy → new system migration, rủi ro lớn nhất không phải là bug hiển thị dễ thấy, mà là thay đổi âm thầm hành vi so sánh/normalize so với hệ thống cũ — dữ liệu "đã đúng" theo rule cũ có thể trở nên "sai" theo rule mới nếu charset/collation/normalization không được kế thừa có chủ đích.
Japanese Text Development — Golden Rules
- Một ký tự tiếng Nhật không phải một byte — luôn dùng hàm đếm ký tự multi-byte (
mb_strlen), không dùngstrlen. - IME composition không phải final input — không bao giờ validate/filter tại
keydown/keypress. - Full-width và Half-width là hai representation khác nhau của cùng một khái niệm — quyết định convert hay không luôn thuộc về business, không phải mặc định kỹ thuật.
ア(full-width katakana) vàア(half-width katakana) là hai code point khác nhau — không tự động coi là tương đương nếu chưa có quyết định rõ ràng.- Dakuten/Handakuten (
は/ば/ぱ,ソ/ゾ) tạo ra các từ có nghĩa hoàn toàn khác nhau — không bao giờ coi là lỗi chính tả hay biến thể của nhau. - Unicode normalization (NFC/NFKC) hữu ích cho search/so sánh, nhưng có thể làm mất thông tin khi áp dụng lên nội dung hiển thị — dùng đúng chỗ.
- Database collation ảnh hưởng trực tiếp đến
=,LIKE,JOIN,GROUP BY,UNIQUE— một collation "hiện đại" (unicode_ci) không đồng nghĩa với "đúng nghiệp vụ" cho dữ liệu tiếng Nhật. - Luôn test bằng dữ liệu tiếng Nhật thật (kèm dakuten, kana nhỏ, full-width/half-width), không chỉ test bằng
ABC/abc. - Luôn thêm UTF-8 BOM khi export CSV cho người dùng mở bằng Excel.
- Mojibake (
文字化け) hầu như luôn xuất phát từ một bước decode/encode sai ở đâu đó trong chuỗi xử lý — debug bằng cách kiểm tra từng tầng, không đoán mò. - Không silently transform (convert/normalize) dữ liệu người dùng nhập nếu chưa có business approval rõ ràng.
- Furigana thường bắt buộc là Full-width Katakana — đây là convention phổ biến trong form hành chính Nhật Bản.
- Số điện thoại, mã bưu điện, số lượng luôn nên convert full-width digit → half-width trước khi xử lý logic.
- SKU/Style No/mã sản phẩm nên giới hạn nghiêm ngặt Latin + số half-width, reject mọi loại ký tự khác.
- Paste, autofill, drag-drop phải đi qua cùng một validation logic như khi gõ tay — không được bỏ sót các luồng input này.
- Frontend validation phục vụ UX; backend validation là nguồn sự thật bắt buộc — không bao giờ chỉ dựa vào validate ở frontend.
- Khi migrate từ legacy system, phải audit charset/collation/normalization thật của dữ liệu gốc, không mặc định dùng cấu hình "hiện đại" của framework mới.
- Tách biệt collation dùng cho exact match nghiệp vụ (UNIQUE, JOIN) khỏi collation/engine dùng cho fuzzy search.
- Mỗi input field cần một quyết định rõ ràng, được document: ký tự nào cho phép, có normalize không, validate ở đâu.
- Không có quy tắc tuyệt đối "tiếng Nhật phải Full-width" hay "phải Half-width" — luôn bắt đầu từ business requirement.
- Emoji và một số ký tự CJK hiếm cần 4 bytes — bắt buộc dùng
utf8mb4, không dùngutf8/utf8mb3. VARCHAR(N)giới hạn N ký tự, không phải N bytes — nhưng giới hạn index/storage thực tế tính theo bytes, cần lưu ý vớiutf8mb4.- Luôn test trên cả JIS keyboard, US keyboard + IME, và mobile keyboard — hành vi có thể khác nhau giữa các nền tảng.
Kết Luận
Xử lý text tiếng Nhật đúng cách không phải là một tính năng phụ, mà là một phần cốt lõi của chất lượng sản phẩm khi phục vụ thị trường Nhật Bản. Từ việc hiểu đúng bản chất Hiragana/Katakana/Kanji, phân biệt rạch ròi Full-width/Half-width, tôn trọng cơ chế IME khi validate input, đến việc chọn đúng charset/collation ở tầng database và luôn nhớ thêm BOM khi export CSV — mỗi quyết định nhỏ đều có thể là ranh giới giữa một hệ thống hoạt động đúng và một hệ thống âm thầm làm sai dữ liệu mà không hề báo lỗi.
Nguyên tắc xuyên suốt cần ghi nhớ: không có rule kỹ thuật tuyệt đối, mọi quyết định về allow/reject/normalize/convert phải xuất phát từ business requirement, được document rõ ràng, và được test bằng dữ liệu tiếng Nhật thật — không phải chỉ ABC/abc — trước khi đưa lên production.
