Japanese Text Breaks Your App in 7 Quiet Ways (With Code)
Your app passes every test in English. Then a Japanese user types their name, presses Enter, and the form submits half a word. Japanese text breaks a lot of quiet assumptions: that words have spaces, that length counts
Your app passes every test in English. Then a Japanese user types their name, presses Enter, and the form submits half a word.
Japanese text breaks a lot of quiet assumptions: that words have spaces, that length counts characters, that Enter means "send", that sorting by string sorts by sound. None of these show up as errors. They just make the app feel slightly broken to Japanese users.
I'm a native Japanese developer. Here are the seven I see most, with code you can copy. (In part 1 I covered checkout forms: names, furigana, addresses and prices.)
1. Enter during IME input is not "send"
Japanese is typed through an IME (input method). You type kaigi, see ใใใ, press Space to convert it to ไผ่ญฐ, then press Enter to confirm the conversion. If your chat box or search field sends on Enter, it sends the half-finished text.
This is the most common bug I see in chat apps, comment boxes and to-do apps built outside Japan.
input.addEventListener('keydown', (e) => {
if (e.key !== 'Enter') return;
// Enter while converting Japanese text: let the IME handle it
if (e.isComposing || e.keyCode === 229) return;
e.preventDefault();
send(input.value);
});
isComposing is true while the IME is converting. The keyCode === 229 check covers older Safari, which fires the confirming Enter after composition has already ended.
React users: check e.nativeEvent.isComposing.
2. length doesn't count characters
'๐ ฎท้ๅฎถ'.length; // 4 (๐ ฎท is outside the BMP: 2 UTF-16 units)
[...'๐ ฎท้ๅฎถ'].length; // 3 (code points)
Code points still aren't what a user calls "one character". Emoji with joiners are several code points:
const seg = new Intl.Segmenter('ja', { granularity: 'grapheme' });
[...'๐จโ๐ฉโ๐งๅฎถๆ'].length; // 7
[...seg.segment('๐จโ๐ฉโ๐งๅฎถๆ')].length; // 3 โ what the user sees
So when you cut text to fit ("max 20 characters", previews, notifications), cut by graphemes, or you'll slice a character in half and show a broken symbol:
const truncate = (text, max) => {
const chars = [...seg.segment(text)].map((s) => s.segment);
return chars.length > max ? chars.slice(0, max).join('') + 'โฆ' : text;
};
truncate('๐ ฎท้ๅฎถใฎ็ไธผ๐จโ๐ฉโ๐งใใฟใใชใง', 8); // "๐ ฎท้ๅฎถใฎ็ไธผ๐จโ๐ฉโ๐งใโฆ"
Also: Japanese is dense. 20 Japanese characters say roughly what 40โ60 English characters say. A limit that's fine in English can be roomy in Japanese โ but each character is about twice as wide, so the pixel width often grows. Don't give buttons a fixed width tuned for English.
3. Line breaks: no spaces, and some characters can't start a line
Japanese has no spaces between words, so browsers may break a line between any two characters โ including in the middle of a word.
Two CSS properties help:
:lang(ja) {
line-break: strict; /* never start a line with ใใใผ or small kana like ใ */
word-break: auto-phrase; /* break between phrases, not inside words (Chrome/Edge 119+) */
}
.title, .button-label {
text-wrap: balance; /* evens out short headings */
}
auto-phrase only works when the element is marked lang="ja", and browsers that don't support it simply keep the normal behaviour, so it's safe to add.
For headings, the most reliable fix is still to keep them short enough to fit on one line on a phone.
4. Don't build sentences by joining strings
Word order is different. English puts the verb first; Japanese puts it last:
| English | Japanese |
|---|---|
| Delete 3 files? | 3ไปถใฎใใกใคใซใๅ้คใใพใใ๏ผ |
| Sent to Yamada | ๅฑฑ็ฐๆงใซ้ไฟกใใพใใ |
So this can't be translated:
// โ the translator only sees fragments
t('delete') + ' ' + count + ' ' + t('files') + '?'
Give the translator the whole sentence with a placeholder:
// โ
const messages = {
en: { confirmDelete: 'Delete {count} files?' },
ja: { confirmDelete: '{count}ไปถใฎใใกใคใซใๅ้คใใพใใ๏ผ' },
};
Plurals are simpler in Japanese โ there's only one form:
new Intl.PluralRules('en').select(1); // "one"
new Intl.PluralRules('ja').select(1); // "other"
new Intl.PluralRules('ja').select(2); // "other"
But Japanese uses counters instead: 3*ไปถ* (items, results), 3*ๅ* (objects), 3*ไบบ* (people), 3*ๆ* (flat things like tickets). The counter belongs to the noun, so keep it inside the translated string, not in your code.
Lists, too:
new Intl.ListFormat('ja', { type: 'conjunction' })
.format(['ใใใ', 'ใฟใใ', 'ใถใฉใ']); // "ใใใใใฟใใใใถใฉใ"
5. Sorting names by string doesn't sort by sound
Intl.Collator('ja') sorts hiragana and katakana together in ใใใใใ order:
['ใชใณใด', 'ใใ', 'ใใ', 'ใคใใด', 'ใฐใชใช', 'ใใใ']
.sort(new Intl.Collator('ja').compare);
// ["ใใ", "ใคใใด", "ใใ", "ใใใ", "ใฐใชใช", "ใชใณใด"] โ
But kanji don't carry their reading. Sorting kanji names gives an order no Japanese person would expect:
['ๅฑฑ็ฐ', 'ไฝ่ค', '้ดๆจ', '็ฐไธญ'].sort(new Intl.Collator('ja').compare);
// ["ไฝ่ค", "ๅฑฑ็ฐ", "็ฐไธญ", "้ดๆจ"]
// expected (by reading): ไฝ่ค (ใใจใ), ้ดๆจ (ใใใ), ็ฐไธญ (ใใชใ), ๅฑฑ็ฐ (ใใพใ )
This is why Japanese forms ask for the name's reading (furigana). If you list people or customers, sort by the reading field, not the name.
6. Search: make hiragana, katakana and half-width match
The same word can be typed in several ways: ใใพใ , ใคใใ, ๏พ๏พ๏พ๏พ (half-width). Users expect search to find all of them. Normalize both the query and the data the same way:
const toKatakana = (s) =>
s.replace(/[ใ-ใ]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 0x60));
const fold = (s) => toKatakana(s.normalize('NFKC').toLowerCase());
fold('๏พ๏พ๏พ๏พ') === fold('ใใพใ '); // true (both become "ใคใใ")
fold('๏ผก๏ผข๏ผฃ๏ผ๏ผ๏ผ'); // "abc123"
NFKC turns half-width katakana and full-width letters into their normal forms; the second step folds hiragana into katakana.
For splitting text into words (search indexing, highlighting), Intl.Segmenter works for Japanese too:
const words = new Intl.Segmenter('ja', { granularity: 'word' });
[...words.segment('ไปๆฅใฏๆฑไบฌใงไผ่ญฐใใใใพใ')]
.filter((s) => s.isWordLike)
.map((s) => s.segment);
// ["ไปๆฅ", "ใฏ", "ๆฑไบฌ", "ใง", "ไผ่ญฐ", "ใ", "ใใ", "ใพใ"]
It's not as good as a real Japanese tokenizer, but it's built in and much better than splitting on spaces (there are none).
7. The same character can look Chinese
Chinese and Japanese share many characters with the same Unicode code point, but the shapes differ slightly (็ด, ้ชจ, ่ง, ๅ are classic examples). If the font is chosen for Chinese, Japanese readers notice โ it looks like a typo.
The fix is to tell the system the text is Japanese:
<html lang="ja">
:lang(ja) {
font-family: "Hiragino Sans", "Noto Sans JP", "Yu Gothic UI", "Yu Gothic", sans-serif;
}
On mobile, the same idea: set the app or view's locale to Japanese when showing Japanese, and in games, make sure the font you ship is a Japanese font (Noto Sans JP is free), not a Chinese one.
Mixed content is the tricky case โ for example, a Chinese user's comment shown inside a Japanese page. Mark it with its own lang attribute if you know it.
Checklist
- [ ] Enter during IME conversion doesn't send (
isComposing) - [ ] Character limits and truncation count graphemes (
Intl.Segmenter) - [ ]
line-break: strictandword-break: auto-phraseon Japanese text; no fixed-width buttons - [ ] Whole sentences with placeholders; counters inside the translation
- [ ] People sorted by reading, not by kanji
- [ ] Search folds hiragana / katakana / half-width (
NFKC) - [ ]
lang="ja"and a Japanese font
None of this is translation. Your Japanese text can be perfect and the app can still feel "not made for here" because of these seven. The good news is they're all small fixes.
If you want the full list, there's a free printable Japan-Ready Checklist (31 points across language, names, addresses, checkout, layout and store pages). And if you'd like a native Japanese developer to go through your actual screens, I do Japan-ready reviews on Fiverr.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.