Dev.to WebDev ๐Ÿ›  Dev ๐Ÿ‘ 0 ๐Ÿ“– 5 min read

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: strict and word-break: auto-phrase on 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.

๐Ÿ“ฐ Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.