PlantUML multi-language notes — CJK fonts, RTL, Unicode, emoji

puml.online

PlantUML is JVM-based, theoretically Unicode-friendly — but Chinese / Japanese / Arabic / emoji each bite in different ways. This post collects all the pitfalls in one go.

CJK: Chinese / Japanese / Korean

Defaults render as “tofu”

PlantUML defaults to Java Logical Font — Linux has no CJK font → renders as □□□.

1
Alice -> Bob: 你好  # renders as "□□□"

Fix: specify a font

1
2
3
4
5
@startuml
skinparam defaultFontName "Noto Sans CJK SC"
Alice -> Bob: 你好
Bob --> Alice: 你好
@enduml

Common CJK fonts that work:

Font System Recommendation
Noto Sans CJK SC Linux/cross-platform ⭐⭐⭐⭐⭐
Noto Sans CJK TC Taiwan-traditional ⭐⭐⭐⭐⭐
Source Han Sans SC macOS/Adobe ⭐⭐⭐⭐⭐
Microsoft YaHei Windows ⭐⭐⭐
PingFang SC macOS Chinese ⭐⭐⭐⭐
WenQuanYi Micro Hei Linux legacy ⭐⭐
Sarasa Mono SC Monospace ⭐⭐⭐

Font fallback

If a font is missing a Unicode character, it should fallback to another font. In PlantUML, this is limited support:

1
2
3
@startuml
skinparam defaultFontName "Noto Sans CJK SC, Noto Sans, Arial"
@enduml

But PlantUML passes font-family to the underlying Graphviz / Skia renderer — actual fallback behavior depends on the engine; not guaranteed to work 100%.

A more robust approach: ensure one font covers all the character ranges you need.

Font installation: server side

If you use plantuml-server (docker):

1
2
3
# Install Noto in the Dockerfile
FROM plantuml/plantuml-server:latest
RUN apt-get update && apt-get install -y fonts-noto-cjk

The official plantuml/plantuml-server image already bundles Noto CJK.

Font installation: client side

If you use TeaVM PlantUML (browser runs plantuml.js), TeaVM itself only ships limited fonts — CJK shows as squares.

Self-host TeaVM + ship your own fonts:

1
2
3
4
5
6
7
<!-- Inject fonts into the page -->
<style>
@font-face {
font-family: 'Noto Sans CJK SC';
src: url('/fonts/NotoSansCJKsc-Regular.woff2') format('woff2');
}
</style>

But PlantUML renders into Canvas / SVG — @font-face doesn’t auto-inject into Canvas. You need either inline font-family on the SVG <text> node + fonts embedded base64:

1
2
3
4
5
6
7
// Post-process after render
async function injectFontIntoSVG(svgText, fontBase64) {
return svgText.replace(
/font-family="[^"]*"/g,
`font-family="Noto Sans CJK SC" data-font="${fontBase64}"`
);
}

Note: browsers’ support for SVG <text> element’s data-font custom field is limited. The stable solution is to change PlantUML’s font config — TeaVM-bundled fonts can be re-packaged.

Font inlining (woff2 base64)

If you have to bundle inside SVG:

1
2
3
4
5
# Subset Noto Sans CJK SC to only the characters you need
pyftsubset NotoSansCJKsc-Regular.otf \
--output-file=noto-subset.woff2 \
--flavor=woff2 \
--text="你好世界验证码登录"

Yields ~150KB woff2 → base64 into SVG (SVG suddenly grows by 100KB).

Unicode character ranges

CJK punctuation

1
2
3
4
5
@startuml
Alice -> Bob: 你好(中文括号)
Alice -> Bob: 「书名号」
Alice -> Bob: 引号"双引号"
@enduml

Most CJK fonts (Noto / 思源黑体) cover CJK punctuation:

  • Chinese parentheses: ()
  • Full-width space:  
  • Chinese punctuation: ,。;:!?
  • Quotation marks: 「」"" 『』
  • Em dash: ——

Emojis

PlantUML supports emoji but depends on the underlying render engine:

1
2
3
4
@startuml
Alice -> Bob: :smile:
Bob --> Alice: :tada:
@enduml

Note :smile: is a PlantUML custom shortcode, only active in preprocessor $ mode.

For literal emoji:

1
2
3
@startuml
Alice -> Bob: 😊
@enduml
Font Emoji support Notes
Noto Sans CJK Partial Old versions lack emoji
Apple Color Emoji macOS system font
Noto Color Emoji Linux/Android font
Segoe UI Emoji Windows system font

PlantUML defaults don’t guarantee emoji rendering — depends on which font library you reference.

Combining diacritics

a + ̃ diacritic combinations (Vietnamese, Arabic) usually render but kerning may be off.

1
2
3
4
5
@startuml
actor "Tô" as T
actor "Über" as U
T --> U
@enduml

RTL (Arabic / Hebrew)

PlantUML’s layout uses Graphviz dotnaturally supports RTL:

1
2
3
4
5
6
7
@startuml
skinparam backgroundColor #fafafa
actor "أحمد" as A
actor "محمد" as M
A -> M: مرحبا
M --> A: أهلاً
@enduml

But caveats:

  • Node names with RTL are fine.
  • Message text with RTL → automatically bidi-mirrored (layout).
  • Order: whether actor A is “left” or “right” depends on bidi algorithm.

If you want to force an RTL-rendered layout:

1
2
3
4
5
6
7
@startuml
left to right direction
' But this option only affects layout direction, not single-line text direction
actor "用户" as User
actor "نظام" as Sys
User --> Sys
@enduml

PlantUML limit: single-line RTL text with automated bidi frequently produces weird layouts for diagram titles / layout-bearing RTL. You may need to manually tweak top to bottom direction or left to right direction.

Arabic / Hebrew in a CJK font

If you pick Noto Sans CJK SC, it doesn’t cover Arabic. Multi-font fallback:

1
2
3
4
@startuml
skinparam defaultFontName "Noto Sans CJK SC, Noto Sans Arabic"
actor "محمد" as M
@enduml

But actual fallback depends on the underlying engine — test to verify which font takes effect.

Monospace / code

Code scenarios need monospace:

1
2
3
4
5
6
7
@startuml
skinparam defaultFontName "JetBrains Mono, Noto Sans Mono CJK SC"
note right of Bob
Code:
```python
def hello():
print("你好")

end note
@enduml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

**Noto Sans Mono CJK SC** covers CJK + monospace in one font.

## Real-world rendering verification flow

### 1. Write a one-line test puml

```plantuml
@startuml
skinparam defaultFontName "Noto Sans CJK SC"
Alice -> Bob: 你好世界 (中文测试)
Alice -> Bob: 🎉🚀 emoji 测试
Alice -> Bob: αβγδ 数学希腊字母
Alice -> Bob: Русский язык кириллица
@enduml

2. plantuml CLI render

1
plantuml -tsvg test.puml

Open SVG in Chrome / Safari:

  • ✅ Chinese renders
  • ⚠️ Emoji squares → font missing
  • ✅ Greek letters render
  • ⚠️ Cyrillic squares → font missing

3. Fix and re-render

1
2
3
@startuml
skinparam defaultFontName "Noto Sans CJK SC, Noto Color Emoji, Noto Sans"
@enduml

4. CI verification

1
2
3
4
5
6
7
8
9
# Render → check for CJK characters in SVG
plantuml -tsvg ci-test.puml
for ch in 你 好 世 界 🎉; do
if grep -q "$ch" ci-test.svg; then
echo "✅ $ch found"
else
echo "❌ $ch missing"
fi
done

A more robust approach: SVGO-compact the SVG, extract <text> node actual character content.

PlantUML’s internal Unicode handling

File encoding

.puml files must be saved as UTF-8 — PlantUML’s hard requirement. Files saved as ANSI on Windows will fail or produce mojibake.

BOM (byte-order mark)

Avoid it — PlantUML 1.2020+ parser accepts BOM but does not guarantee. Recommendation:

1
2
# Strip UTF-8 BOM
sed -i '1s/^\xef\xbb\xbf//' file.puml

Team collaboration: multi-language practice

A common pain

“I see □□□, my colleague sees Chinese”

Root cause: team members have different local fonts. Most stable approach:

  1. CI renders diagram → screenshot → bundle in release
  2. Rendered output doesn’t contain fonts (SVG doesn’t embed font files)
  3. Viewers without Noto installed → see tofu

Cure: SVG embeds woff2 base64. Or use PDF output (clients often auto-fallback fonts).

File organisation suggestion

1
2
3
4
5
6
7
8
9
10
docs/diagrams/
├── system.puml # English source (maintain)
├── system-i18n/
│ ├── system_zh-CN.svg # Chinese render result
│ ├── system_en.svg
│ ├── system_ja.svg
│ └── ...
├── fonts/
│ ├── NotoSansCJKsc-Regular.woff2
│ └── README.md # font license

Every puml edit → CI renders all language SVGs → upload to wiki.

PlantUML’s i18n pattern

puml doesn’t have native i18n functions, but !define + $ variable substitution works:

1
2
3
4
5
6
7
8
9
10
@startuml
!$zh = "你好"
!$en = "Hello"

!if %variable("LANG") == "zh"
Alice -> Bob: %zh
!else
Alice -> Bob: %en
!endif
@enduml

The environment variable needs to be injected when calling plantuml CLI:

1
plantuml -DLANG=zh -tdefault test.puml

Multi-language source code in git

Store per-language source in git:

1
2
3
4
5
docs/diagrams/
├── en/system.puml
├── zh-CN/system.puml
├── ja/system.puml
└── render.sh
1
2
3
4
5
6
7
#!/bin/bash
for lang in en zh-CN ja; do
for f in docs/diagrams/$lang/*.puml; do
name=$(basename "$f" .puml)
plantuml -tsvg "$f" -o "static/img/diagrams/$lang/"
done
done

CI runs: bash render.sh → publish to wiki.

Advanced: CJK monospace / proportional mix

1
2
3
4
5
6
7
8
9
@startuml
skinparam defaultFontName "Inter"
note right of Bob
Mixed:
Chinese 中文 + English with **Markdown**
- Item 1
- Item 2
end note
@enduml

Font fallback chain:

  • Inter → Latin letters look nice
  • Hits CJK characters → fallback to Noto Sans CJK SC (if fallback set)

PlantUML + skinparam multi-font syntax:

1
2
3
4
skinparam defaultFontName "Inter"
skinparam sequence {
ParticipantFontName "Inter, Noto Sans CJK SC"
}

Practical checklist

  1. Production use Noto Sans CJK SC — open source, full coverage, easy to install on Linux/Mac/Windows
  2. CI servers install CJK fontsapt-get install fonts-noto-cjk when building docker image
  3. Don’t bother with TeaVM client + CJK — font injection is too painful, switch to plantuml-server
  4. !theme cyborg / !theme dark — avoid font blacklists
  5. CJK characters are wide — Graphviz dot accounts for CJK width, skinparam nodesep 40 for breathing room
  6. skinparam dpi — pair with CSS so diagrams stay sharp when scaled

Decision for puml.online specifically

Our project:

  • Most diagrams have CJK annotations → Noto Sans CJK SC
  • TeaVM WASM editor rendering → fallback to plantuml.com (TeaVM lacks CJK)
  • CI render → docker image bundles Noto

No need to extend the Unicode complexity.

Recap

  • PlantUML’s Unicode story has three layers: font availability, RTL algorithm, emoji compatibility.
  • CJK’s main pitfall: font is missing — JVM default font has no CJK.
  • Noto Sans CJK SC + server-side docker built-in solves 95% of production cases.
  • TeaVM + client: abandon CJK, switch to plantuml.com.

Next

  • Title: PlantUML multi-language notes — CJK fonts, RTL, Unicode, emoji
  • Author: puml.online
  • Created at : 2026-07-30 12:11:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-multilingual-notes-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.