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'); // "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.