CJK fonts on Arch Linux: installing them is the easy part

Installing Chinese fonts on Arch is one pacman command. Getting fontconfig to hand the right one to sans-serif is the actual work, and it's why your UI digits look heavy.

Installing CJK fonts on Arch is one pacman command. Getting fontconfig to hand the right one to sans-serif is where the actual work is, and it's the part nobody writes down.

The symptom that sent me down this road: after removing wqy-zenhei, Chinese text in my UI started looking wrong. Letter spacing was off, digits looked heavy. The fonts were all installed. Nothing was missing. The problem was that a monospace font had quietly won the sans-serif fallback slot for Chinese, and fontconfig was doing exactly what I'd told it to do, which was nothing.

This is the setup I landed on. It's one Arch box, not universal gospel, but the debugging section applies anywhere fontconfig runs.

The short version

Role

Font

Latin UI

Inter / Noto Sans

CJK UI

MiSans (closest to macOS PingFang), or HarmonyOS Sans SC

CJK body text

Noto Serif CJK SC or Source Han Serif CN

CJK reading / ebooks

LXGW WenKai

Monospace

Maple Mono NF CN (Nerd Font glyphs included)

Emoji

Noto Color Emoji

Installing the packages is not enough. You must set fontconfig priorities explicitly, or a monospace font will steal the sans-serif CJK fallback slot.

Installing

# From the official repos
sudo pacman -S \
  noto-fonts \
  noto-fonts-cjk \
  noto-fonts-emoji \
  noto-fonts-extra \
  inter-font

# From the AUR
paru -S maplemono-nf-cn                    # Maple Mono NF CN, the monospace pick

# From the AUR, pick one as your modern CJK UI face
paru -S otf-misans misans-fontconfig       # Xiaomi MiSans
# or
paru -S ttf-harmonyos-sans                 # Huawei HarmonyOS Sans

Run fc-cache -fv afterwards.

Sans-serif, for UI and web

Package

Font

Notes

otf-misans + misans-fontconfig

MiSans

My pick. Closest thing to PingFang, ships its own fontconfig

ttf-harmonyos-sans

HarmonyOS Sans SC

Same tier as MiSans, more geometric

otf-alibaba-puhuiti

AlibabaPuHuiTi 3

Wide weight range, neutral

noto-fonts-cjk

Noto Sans CJK SC

Install it regardless as the safety net. Slightly soft letterforms

adobe-source-han-sans-cn-fonts

Source Han Sans CN

Same source as Noto Sans CJK. Switching between them accomplishes nothing

inter-font

Inter

Latin companion

Serif, for reading

Package

Font

noto-fonts-cjk

Noto Serif CJK SC (same package as the sans)

adobe-source-han-serif-cn-fonts

Source Han Serif CN

noto-fonts

Noto Serif

Kai / long-form screen reading

ttf-lxgw-wenkai from archlinuxcn gives you LXGW WenKai, a screen-optimised kai face. Best option for ebooks and long articles.

Monospace

Package

Font

Notes

maplemono-nf-cn

Maple Mono NF CN

From the AUR. Nerd Font icons and CJK in one package. Recommended

ttf-jetbrains-mono-nerd

JetBrains Mono Nerd

Ligatures and icons

ttf-firacode-nerd

Fira Code Nerd

The classic ligature font

ttf-cascadia-code

Cascadia Code

Microsoft, ligatures

ttf-sarasa-gothic

Sarasa Mono SC

Strict 1:2 CJK-to-Latin metrics. 800MB, only if you need it

Emoji and compatibility

noto-fonts-emoji is mandatory. ttf-twemoji and ttf-openmoji are alternatives if you dislike the Google style. For document compatibility, ttf-liberation gives you metric-compatible Arial/Times/Courier substitutes, and ttf-dejavu is a reasonable last-resort fallback.

What to skip

`wqy-zenhei` / `wqy-microhei`. Embedded bitmaps make them mushy at small sizes. Fully superseded by Noto CJK and MiSans.

`adobe-source-han-sans-otc-fonts`. Conflicts with the cn-fonts variant. Use the CN one.

Installing several monospace fonts without touching fontconfig. This is the one that bites. A monospace font can win the sans-serif CJK match and your UI ends up with heavy digits and broken spacing.

Debugging what you actually have

Look before you edit. Every command here is read-only.

Which font does a generic family resolve to?

fc-match asks fontconfig the real question: if someone requests this family, what do you hand them?

# Default sans-serif, no language constraint, usually a Latin font
fc-match sans-serif
# → NotoSans-Regular.ttf: "Noto Sans" "Regular"

# With a language constraint. This is what a web page actually gets
# when Chinese text shows up.
fc-match "sans-serif:lang=zh-cn"
fc-match "serif:lang=zh-cn"
fc-match "monospace:lang=zh-cn"

fc-match emoji

# Simulate a page whose CSS hardcodes a font you don't have
fc-match "PingFang SC:lang=zh-cn"

The full fallback chain

This is the most useful one. -s prints the entire sorted list fontconfig walked. The first line is what you actually get.

fc-match -s "sans-serif:lang=zh-cn" | head -10
fc-match -s "monospace:lang=zh-cn" | head -10

The first font in the chain that covers the target characters wins. Mushy text, heavy digits, weird spacing: it's almost always because line one is wrong.

Match by codepoint

More precise than a language tag when you want to know which font draws a specific character.

# U+4E2D
fc-match -s "sans-serif:charset=4E2D" | head -3

# U+1F600, an emoji
fc-match -s "sans-serif:charset=1F600" | head -3

# Several at once
fc-match -s "sans-serif:charset=4E2D 4E00 6587 5B57" | head -3

Why does this font look bad?

-v dumps every fontconfig property for the match. Useful when a font renders badly but you don't know which knob is responsible.

fc-match -v "sans-serif:lang=zh-cn" | grep -E "antialias|hinting|hintstyle|rgba|autohint|embeddedbitmap"

Which config files are loaded?

Load order determines final priority.

fc-conflist | head -40

You'll see /etc/fonts/conf.d/*.conf, /etc/fonts/conf.avail/*, and your own ~/.config/fontconfig/fonts.conf. Your user config should appear at the end of the list, otherwise it isn't overriding anything.

Trace the matching itself

When you've added a prefer block and it stubbornly does nothing, FC_DEBUG explains why.

# 1024 = MATCHV, verbose matching
FC_DEBUG=1024 fc-match "sans-serif:lang=zh-cn" 2>&1 | head -80

# 4 = CONFIG, config file application
FC_DEBUG=4 fc-match "sans-serif:lang=zh-cn" 2>&1 | head -40

Value

Meaning

1

MATCH, basic matching

4

CONFIG, config loading

1024

MATCHV, verbose matching

16384

OBJTYPES, font properties

-1

Everything. Extremely noisy

Listing installed fonts

fc-list | wc -l
fc-list :lang=zh-cn | head -20

# Custom format: family | style | file
fc-list -f '%{family[0]}|%{style[0]}|%{file}\n' :lang=zh-cn | head -20

# Which language tags exist on this system
fc-list :lang | tr ',' '\n' | sort -u | head -20

A one-shot diagnostic

Save this as ~/bin/font-doctor.sh or paste it straight into a terminal.

echo "===== Generic family, Chinese ====="
for g in sans-serif serif monospace; do
  printf "%-12s -> " "$g"
  fc-match "$g:lang=zh-cn"
done

echo
echo "===== Fallback chain, top 5 (sans-serif zh-cn) ====="
fc-match -s "sans-serif:lang=zh-cn" | head -5

echo
echo "===== Emoji ====="
fc-match emoji

echo
echo "===== Is the user config loaded? ====="
fc-conflist | grep -E "fontconfig/fonts.conf|conf.d" | tail -5

Configuring fontconfig properly

Installing fonts is not configuration. You have to declare CJK priority for all three generic families explicitly. Without that, fontconfig falls back in whatever order happens to cover the character set first, and a monospace font like Maple Mono frequently wins the sans-serif slot.

Principles

  1. Only touch user config. ~/.config/fontconfig/fonts.conf. Never edit /etc/fonts/conf.d/.
  2. Use `<alias>` with `<prefer>`. fontconfig walks the prefer list in order and stops at the first font covering the target charset for that language.
  3. Latin fonts first, CJK right behind them. Latin fonts contain no CJK, so fontconfig automatically falls through to the first CJK font in the chain. The structure stays readable.
  4. Emoji last, so emoji codepoints reliably land somewhere.
  5. Don't use `<rejectfont>` to ban a font globally. It rejects that font everywhere, including terminals and explicit requests. To exclude something from fallback, just leave it out of the alias chain. A font that isn't in the chain never gets reached by alias resolution.

Applying it

mkdir -p ~/.config/fontconfig
$EDITOR ~/.config/fontconfig/fonts.conf
# paste the template below
fc-cache -fv

Applications pick it up on restart. No need to log out of your desktop session.

The template

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>

  <!-- ============ sans-serif: UI, web, apps ============ -->
  <alias binding="strong">
    <family>sans-serif</family>
    <prefer>
      <!-- Latin first -->
      <family>Inter</family>
      <family>Noto Sans</family>
      <!-- CJK fallback, in order -->
      <family>MiSans</family>
      <family>Noto Sans CJK SC</family>
      <family>Source Han Sans CN</family>
      <!-- Emoji last -->
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>

  <!-- ============ serif: body text, reading ============ -->
  <alias binding="strong">
    <family>serif</family>
    <prefer>
      <family>Noto Serif</family>
      <family>Noto Serif CJK SC</family>
      <family>Source Han Serif CN</family>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>

  <!-- ============ monospace: terminal, editors ============ -->
  <alias binding="strong">
    <family>monospace</family>
    <prefer>
      <family>Maple Mono NF CN</family>
      <family>Noto Sans Mono CJK SC</family>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>

  <!-- ============ Rendering, adjust to taste ============ -->
  <match target="font">
    <edit name="antialias" mode="assign"><bool>true</bool></edit>
    <edit name="hinting" mode="assign"><bool>true</bool></edit>
    <edit name="hintstyle" mode="assign"><const>hintslight</const></edit>
    <edit name="rgba" mode="assign"><const>rgb</const></edit>
    <edit name="lcdfilter" mode="assign"><const>lcddefault</const></edit>
    <edit name="embeddedbitmap" mode="assign"><bool>false</bool></edit>
  </match>

</fontconfig>

Syntax worth knowing

Element

Meaning

<alias>

Maps a generic family to a set of real fonts

<prefer>

High-priority fallback chain, taken in order

<accept>

Low-priority fallback. Rarely needed

<default>

Final fallback when no alias matches

binding="strong"

Outranks same-named rules from /etc/fonts/conf.d/. Worth adding

<match target="font">

Changes rendering properties: hinting, antialias, rgba

<match target="pattern">

Changes the request itself, e.g. rewriting "PingFang SC" to "MiSans"

Verifying

fc-match "sans-serif:lang=zh-cn"     # expect MiSans or Noto Sans CJK SC
fc-match "serif:lang=zh-cn"          # expect Noto Serif CJK SC
fc-match "monospace:lang=zh-cn"      # expect Maple Mono NF CN
fc-match emoji                       # expect Noto Color Emoji

fc-match -s "sans-serif:lang=zh-cn" | head -10
fc-match -s "sans-serif:charset=4E2D" | head -3
fc-conflist | head -30

Things that will trip you up

Changes don't apply. You didn't restart the application. fontconfig is read once at process start.

`fc-cache` is slow or errors. Add -f to force a rebuild. The first scan over a large font library takes a minute or two.

One specific site or Electron app still looks wrong. Check whether its CSS hardcodes a font you don't have, like font-family: "PingFang SC". Rewrite the request:

<match target="pattern">
  <test name="family"><string>PingFang SC</string></test>
  <edit name="family" mode="assign" binding="same">
    <string>MiSans</string>
  </edit>
</match>

You want a different font per language. Constrain with <test name="lang">. For example, Japanese only:

<match target="pattern">
  <test name="lang" compare="contains"><string>ja</string></test>
  <test name="family"><string>sans-serif</string></test>
  <edit name="family" mode="prepend" binding="strong">
    <string>Noto Sans CJK JP</string>
  </edit>
</match>

Malformed XML fails silently. It isn't reported, it's just ignored. Use fc-conflist to confirm your file is loaded, and FC_DEBUG=1024 fc-match sans-serif to watch it parse.

Three things I learned the hard way

Removing `wqy-zenhei` made my UI worse, and it wasn't the font's fault. The sans-serif CJK fallback slot got taken over by a monospace font. The fix was writing explicit fontconfig priorities, not reinstalling anything.

Source Han Sans and Noto Sans CJK are the same typeface. Same origin, different branding. Swapping one for the other changes nothing. If you want a different look, go to MiSans or HarmonyOS Sans.

Maple Mono NF CN already renders CJK, so you probably don't need Sarasa Mono. Sarasa's strict 1:2 metrics are genuinely nice, but it's an 800MB package. Only worth it if you specifically need that alignment.

No comments yet