4 ways canvas text rendering breaks in multilingual apps (that en/ja testing will never catch)
I run a large fleet of "preview it, then download it as PNG" web tools - name tags, certificate generators, price cards, badges - in five languages: Japanese, English, Spanish, French, Portuguese. Canvas 2D text rendering looks correct as long as you only test Japanese and English. It breaks when you run es/fr/pt through it. After stepping on these repeatedly, the failures collapse into four patterns.
The premise: Latin languages run 1.4-2× longer than Japanese
Design data first. The same label, measured across five locales:
| Example | ja | en | es | fr | pt |
|---|---|---|---|---|---|
| Tool name | 22 chars | 35 | 62 | 48 | 50 |
| "Standard" button | 4 | 8 | 10 | 20 | 12 |
Rule of thumb: fr/pt come out 1.4-1.7× longer than ja; es can balloon to nearly 3×. A font size and maxWidth tuned to fit Japanese will not fit the Latin locales. All four failure patterns grow from this.
Pattern 1: hand-rolled wrapping via text.split(/\s+/) collapses on CJK
The classic snippet - split on spaces, wrap word by word - does nothing for Japanese or Chinese, where words aren't space-delimited. An entire sentence becomes one unbreakable token and clips at the canvas edge. Test with real Japanese input and check that the final line renders to its last character. "Most of it showed up" is not a pass.
Pattern 2: an ASCII-only tokenizer splits words at accented characters
Fix pattern 1 with a character-class tokenizer like [A-Za-z0-9'\-_] and you've traded one regression for another: ç é ã ó ñ aren't in that class, so produção fragments into produ / ç / ão mid-word. An English test will never catch this. Generate actual PNGs with fr/es/pt samples and eyeball the area around accented characters. I never found another detection method - string-comparison tests can't see a rendering-level split.
Pattern 3: the important word at the end vanishes into "…"
Since fr/pt run 1.4-1.7× longer than the ja the layout was tuned for, text overflows its two lines and gets ellipsized. The cruel part: what disappears is the tail of the phrase - often the semantically critical word, like production / produção. The fix was splitting fields into two classes. Fields that may truncate (descriptions) wrap-and-ellipsize. Fields that must never truncate (titles, names) get auto-fit: measure on an offscreen canvas with measureText, search for a font size that fits, render at that size. A title with an ellipsis is a broken deliverable, so never mix the two strategies.
Pattern 4: the preview and the downloaded PNG break lines in different places
The sneakiest one. Implement pattern 3's auto-fit only on the canvas side while the DOM preview keeps its fixed font size, and the line breaks you see on screen won't match the line breaks in the PNG. From the user's perspective, the tool's core promise - "download what you see" - is broken. The fix: centralize the auto-fit font-size decision in one place (the offscreen canvas measurement) and apply the resulting size to both the canvas render and the preview DOM's style.fontSize. My pass condition: preview line count equals PNG line count, in all five locales.
Pre-release checklist
For every new image-output tool:
- [ ] ja: CJK renders to the last character of the final line, no edge clipping
- [ ] fr / es / pt: generate real PNGs; no word splits around
ç é ã ó ñ - [ ] fr / pt: no ellipsis eating the important trailing words
- [ ] all five locales: preview (DOM) and downloaded PNG agree on line breaks and line count
Declaring "multilingual support: done" after checking only en and ja was the single biggest trap.
Limits and caveats
The character-count ratios are measured from my own fleet (utility-tool names, descriptions, UI labels). Different content genres will shift the ratios. On automation limits: patterns 2 and 4 resist static analysis entirely. Generating PNGs and looking at them was the only detection that worked. Five locales × generation checks is a real cost - whatever you skip surfaces in production instead.
Verified: April-May 2026. Environment: Canvas 2D API / vanilla JS / ja·en·es·fr·pt locales in production.
Top comments (0)
Comments
No comments yet. Start the discussion.