- Tác giả

- Name
- Nguyễn Đức Xinh
- Ngày xuất bản
- Ngày xuất bản
Regex Tiếng Nhật Và Validation Input Trong React
Làm việc cho các dự án của khách hàng từ Nhật Bản, chúng ta sẽ thường xuyên phải validate dữ liệu tiếng Nhật: Hiragana, Katakana, Kanji, và đặc biệt là hai dạng Full-width (Zenkaku) và Half-width (Hankaku) của cùng một ký tự. Bài viết này tổng hợp các regular expression thông dụng nhất khi làm việc với dữ liệu tiếng Nhật, kèm theo best practice khi áp dụng chúng, và cách xử lý validation/input đúng cách ở phía Frontend — đặc biệt là với React, nơi một filter viết sai có thể phá vỡ hoàn toàn trải nghiệm gõ tiếng Nhật qua IME.
Nếu bạn chưa nắm khái niệm Full-width/Half-width, nên đọc trước bài Full-width vs Half-width Trong Tiếng Nhật — xét trên một font có chiều rộng/chiều cao cố định, ký tự half-width (hankaku) chiếm nửa bề rộng so với ký tự full-width (zenkaku), chiều cao thì không đổi.
Bảng Regex Thông Dụng Cho Text Tiếng Nhật
| Mục đích | Regex | Ví dụ khớp |
|---|---|---|
| Chữ số Full-width (Zenkaku) | /^[ー]?[0-9]+(\.[0-9]+)?$/ |
123, ー12.5 |
| Chữ số Half-width (Hankaku) | /^[-]?[0-9]+(\.[0-9]+)?$/ |
123, -12.5 |
| Chữ cái Latin Half-width (Hankaku) | /^[a-zA-Z]+$/ |
ABC, abc |
| Hiragana Full-width (Zenkaku) | /^[ぁ-ん]+$/ |
あいうえお |
| Katakana Full-width (Zenkaku) | `/^([ァ-ン] | ー)+$/` |
| Hiragana + Katakana Full-width | /^[ぁ-んァ-ン]+$/ |
あアいイ |
| Katakana Half-width (Hankaku) | /^[ァ-ン゙゚]+$/ |
アイウエオ |
| Kanji | /^[一-龥]+$/ |
日本語 |
| Hiragana Full-width + Kanji | /^[一-龥ぁ-ん]+$/ |
日本のはなし |
| Hiragana + Katakana Full-width + Kanji | /^[ぁ-んァ-ン一-龥]+$/ |
東京タワーへ行く |
| Mã bưu chính (postal code) | /^\d{3}-\d{4}$/ |
123-4567 |
| Số điện thoại cố định | /^[0-9-]{6,9}$|^[0-9-]{12}$/ |
03-1234-5678 |
(Hai regex "Hiragana Full-width + Kanji" và "Hiragana + Katakana Full-width + Kanji" ở trên đã được thêm +$ ở cuối so với bản nháp gốc — thiếu $ khiến regex chỉ kiểm tra ký tự đầu chuỗi, không đảm bảo toàn bộ chuỗi hợp lệ, xem mục Best Practice bên dưới. Mã bưu chính cũng được gộp lại thành một pattern duy nhất đúng chuẩn Nhật Bản 3 số - 4 số, thay vì hai dòng gốc dễ gây nhầm lẫn.)
Best Practice Khi Dùng Regex Tiếng Nhật
1. Luôn anchor cả hai đầu bằng ^ và $
Thiếu $ là lỗi phổ biến nhất khi copy-paste regex: /^[一-龥ぁ-ん]/ chỉ kiểm tra ký tự đầu tiên của chuỗi, nghĩa là "日本abc<script>" vẫn "khớp" vì ký tự đầu là Kanji hợp lệ. Luôn viết đầy đủ /^[...]+$/ để đảm bảo toàn bộ chuỗi chỉ chứa các ký tự cho phép.
2. Cân nhắc dùng Unicode property escape thay vì hardcode range
Các range như [ぁ-ん], [ァ-ン], [一-龥] hoạt động tốt nhưng dễ gõ sai hoặc bỏ sót ký tự biên (ví dụ 龥 không phải là ký tự Kanji cuối cùng theo chuẩn Unicode CJK Unified Ideographs hiện tại). JavaScript hiện đại hỗ trợ Unicode property escape rõ ràng và dễ đọc hơn:
// Thay vì hardcode range:
const kanjiRegexOld = /^[一-龥]+$/
// Nên dùng Unicode script property (cần flag "u"):
const hiraganaRegex = /^\p{Script=Hiragana}+$/u
const katakanaRegex = /^\p{Script=Katakana}+$/u
const kanjiRegex = /^\p{Script=Han}+$/u
Giải thích: \p{Script=Hiragana} khớp với toàn bộ khối Unicode Hiragana chính thức, không phụ thuộc vào việc bạn có gõ đúng range hex hay không. Bắt buộc phải có flag u (Unicode mode) thì cú pháp \p{...} mới hoạt động.
3. Regex không tự động normalize — đừng chỉ Reject, hãy cân nhắc Normalize trước
Một regex half-width digit (/^[-]?[0-9]+(\.[0-9]+)?$/) sẽ reject ngay khi user paste 123 (full-width, do IME auto-convert). Với UX tốt hơn, nên convert full-width → half-width trước khi chạy regex, thay vì báo lỗi ngay cho một giá trị mà user không cố ý gõ sai (xem phần "Khi Nào Nên Normalize" trong bài Full-width/Half-width).
4. Regex chỉ là điều kiện cần, không phải điều kiện đủ
Regex kiểm tra được loại ký tự, nhưng không kiểm tra được ý nghĩa nghiệp vụ — ví dụ /^\d{3}-\d{4}$/ chấp nhận 000-0000 dù đây không phải mã bưu chính thật. Với dữ liệu quan trọng (mã bưu chính, số điện thoại), nên kết hợp thêm một bước xác thực nghiệp vụ (tra cứu bảng mã bưu chính thật, hoặc gọi API xác thực) thay vì chỉ tin vào regex.
5. Không dùng regex để "chặn" input theo từng phím gõ
Đây là lỗi kỹ thuật nghiêm trọng nhất khi áp dụng các regex trên vào input tiếng Nhật — xem chi tiết ngay phần tiếp theo.
Vì Sao Không Nên Validate Bằng Regex Ngay Trên onKeyDown?
Người dùng tiếng Nhật gõ chữ qua IME: gõ romaji trung gian (ka, kyo, nihon) rồi IME mới "chốt" thành Hiragana/Kanji cuối cùng. Nếu bạn chạy regex kiểm tra Hiragana (/^[ぁ-ん]+$/) ngay tại sự kiện onKeyDown, bạn sẽ chặn nhầm chữ k (phím vật lý vừa gõ, chưa phải ký tự cuối cùng) — phá vỡ hoàn toàn khả năng gõ tiếng Nhật của người dùng.
Giải pháp đúng là validate tại compositionend (khi IME đã chốt xong) hoặc blur/submit, không phải tại từng phím gõ. Phần tiếp theo trình bày cách implement chuẩn trong React.
Validation Và Input Tiếng Nhật Trong React
Custom Hook: useJapaneseInput
Một custom hook tái sử dụng được, tách biệt composition state khỏi validation logic:
import { useState, useCallback } from 'react'
type UseJapaneseInputOptions = {
pattern: RegExp
errorMessage: string
normalize?: (value: string) => string
}
export function useJapaneseInput({ pattern, errorMessage, normalize }: UseJapaneseInputOptions) {
const [value, setValue] = useState('')
const [error, setError] = useState('')
const [isComposing, setIsComposing] = useState(false)
const validate = useCallback(
(raw: string) => {
const finalValue = normalize ? normalize(raw) : raw
if (finalValue !== '' && !pattern.test(finalValue)) {
setError(errorMessage)
} else {
setError('')
}
return finalValue
},
[pattern, errorMessage, normalize],
)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
// Bỏ qua validate khi IME đang composition (romaji trung gian, chưa final)
if (!isComposing) validate(e.target.value)
}
const handleCompositionStart = () => setIsComposing(true)
const handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
setIsComposing(false)
const target = e.target as HTMLInputElement
const finalValue = validate(target.value)
// Nếu có normalize, cập nhật lại value hiển thị về dạng canonical
if (normalize) setValue(finalValue)
}
return {
value,
error,
inputProps: {
value,
onChange: handleChange,
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
},
}
}
Giải thích:
patternvàerrorMessageđược truyền vào từ ngoài — hook này dùng chung được cho mọi loại field (Hiragana, Katakana, mã bưu chính...), chỉ cần đổi regex và message.normalizelà optional: dùng cho field cần convert full-width → half-width (ví dụ số điện thoại) trước khi validate, thay vì reject ngay khi IME auto-convert số thành full-width.- Validate chỉ chạy khi
!isComposing(trongonChange) hoặc tạionCompositionEnd— không bao giờ chạy ởonKeyDown.
Áp Dụng Cho Field Furigana (Katakana Full-width Bắt Buộc)
function FuriganaInput() {
const { value, error, inputProps } = useJapaneseInput({
pattern: /^([ァ-ン]|ー| |\s)+$/,
errorMessage: '全角カタカナで入力してください。(Vui lòng nhập bằng Katakana full-width)',
})
return (
<div>
<input {...inputProps} inputMode="text" maxLength={50} placeholder="ヤマダ タロウ" />
{error && <p className="text-red-500 text-sm">{error}</p>}
</div>
)
}
Áp Dụng Cho Field Số Điện Thoại (Normalize Trước Khi Validate)
const toHalfWidthDigits = (str: string) =>
str.replace(/[0-9]/g, (char) => String.fromCharCode(char.charCodeAt(0) - 0xfee0))
function PhoneInput() {
const { value, error, inputProps } = useJapaneseInput({
pattern: /^[0-9-]{6,9}$|^[0-9-]{12}$/,
errorMessage: '半角数字で入力してください。(Vui lòng nhập bằng số half-width)',
normalize: toHalfWidthDigits,
})
return (
<div>
<input {...inputProps} inputMode="tel" maxLength={15} placeholder="03-1234-5678" />
{error && <p className="text-red-500 text-sm">{error}</p>}
</div>
)
}
Giải thích: toHalfWidthDigits convert từng ký tự số full-width (0-9, mã Unicode U+FF10-U+FF19) về half-width bằng cách trừ offset 0xFEE0 — đây là khoảng cách cố định giữa mọi cặp ký tự full-width/half-width tương ứng trong bảng Unicode. Nhờ vậy, user gõ 123 (do IME tự convert) vẫn được chấp nhận, chỉ là hiển thị lại dưới dạng 123 sau khi validate.
Đếm Độ Dài Ký Tự Đúng Cách (Tránh Lỗi Với maxLength)
Với các ký tự nằm ngoài Basic Multilingual Plane (một số Kanji hiếm, emoji), thuộc tính .length của JavaScript string đếm theo UTF-16 code unit, có thể sai lệch so với số ký tự thực tế người dùng nhìn thấy:
// Không chính xác với ký tự surrogate pair
"𠀀".length // 2, dù đây là 1 ký tự
// Chính xác hơn: đếm theo code point
Array.from("𠀀").length // 1
Với validation độ dài (ví dụ giới hạn 20 ký tự tên), nên dùng Array.from(value).length thay vì value.length trực tiếp khi cần đếm chính xác tuyệt đối, dù trong đa số trường hợp Hiragana/Katakana/Kanji thông thường (nằm trong BMP) thì .length vẫn cho kết quả đúng.
Kết Hợp Với React Hook Form + Zod
Khi dùng thư viện form (react-hook-form), regex vẫn áp dụng được bình thường qua zod, nhưng cần lưu ý: validation của react-hook-form chạy trên giá trị đã commit (sau onChange/onBlur), nên composition-safety cần xử lý ở tầng input component (dùng useJapaneseInput ở trên), còn zod chỉ chịu trách nhiệm validate giá trị cuối cùng khi submit:
import { z } from 'zod'
import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
const schema = z.object({
furigana: z
.string()
.min(1, 'Vui lòng nhập フリガナ')
.regex(/^([ァ-ン]|ー| |\s)+$/, '全角カタカナで入力してください。'),
})
function CustomerForm() {
const { control, handleSubmit } = useForm({ resolver: zodResolver(schema) })
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="furigana"
control={control}
render={({ field, fieldState }) => (
<>
<input
{...field}
onCompositionStart={() => {}}
onCompositionEnd={(e) => field.onChange((e.target as HTMLInputElement).value)}
/>
{fieldState.error && <p>{fieldState.error.message}</p>}
</>
)}
/>
<button type="submit">Submit</button>
</form>
)
}
Lưu ý quan trọng: với pattern trên, field.onChange mặc định của react-hook-form gọi ở mỗi onChange — nếu regex có tính "chặt" (reject sớm), lỗi có thể hiển thị trong khi user đang gõ dở qua IME. Cách an toàn hơn là set mode: 'onBlur' hoặc mode: 'onSubmit' trong useForm() cho các field tiếng Nhật, thay vì mode: 'onChange' mặc định — tránh hiện lỗi giữa lúc composition chưa hoàn tất.
Tổng Kết
Bộ regex ở bảng trên là điểm khởi đầu tốt để validate dữ liệu tiếng Nhật, nhưng regex chỉ giải quyết được phần "loại ký tự nào hợp lệ" — phần khó hơn, dễ gây bug hơn, là thời điểm và cách chạy validation trên input tiếng Nhật: luôn tôn trọng composition event của IME, luôn cân nhắc normalize (full-width → half-width) trước khi reject, và luôn anchor đầy đủ ^...$ để tránh lỗi validate hời hợt. Áp dụng đúng các nguyên tắc này trong React — qua custom hook tách biệt composition state khỏi validation logic — là cách chắc chắn nhất để tránh những bug "trông như hoạt động đúng" nhưng thực ra chặn nhầm người dùng Nhật ngay từ bước gõ phím.
