Switching KDE to Chinese: the setting was right, everything under it was wrong

KDE was set to Chinese and the UI stayed English, because the locale had never been generated. Fixing that surfaced Korean glyphs, then a UI one weight too heavy - MiSans ships weight metadata fontconfig reads wrong.

I set KDE to Chinese in System Settings. The UI stayed English.

The setting was not the problem. KDE had written it down correctly in two places, and every translation file was on disk. The problem was one layer down, and after I fixed that one, two more appeared underneath it. Each looked like the previous fix had broken something. None of them had.

Symptom

Actual cause

UI stays English

zh_CN.UTF-8 was never generated, so glibc fell back to C

Chinese renders, glyphs look wrong

fontconfig picked the Korean variant out of the Noto CJK bundle

New fonts don't show up in Plasma

Plasma requests Noto Sans by name and never touches the sans-serif alias

Whole UI is one weight too heavy

MiSans ships weight metadata that fontconfig reads wrong

This is one Arch box — Plasma 6.7.4 on Wayland, kernel 7.1.6-zen — but the diagnosis for each layer generalises.

The locale was never generated

KDE's side was fine:

~/.config/plasma-localerc             LANG=zh_CN.UTF-8 / LANGUAGE=zh_CN
/var/lib/AccountsService/users/jin    Languages=zh_CN.UTF-8;

glibc's side was not:

$ locale
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
LANG=zh_CN.UTF-8                    # the variable is right

$ locale -a
C  C.utf8  POSIX  en_US.utf8        # but zh_CN doesn't exist

$ grep -vE '^\s*#|^\s*$' /etc/locale.gen
en_US.UTF-8 UTF-8                   # only English was ever enabled

The environment variable pointed at a locale that had never been built, so glibc fell back to C/POSIX and every program rendered in English. Not a missing translation package — those were all there:

$ ls /usr/share/locale/zh_CN/LC_MESSAGES/ | wc -l
533

The fix is two steps: uncomment zh_CN.UTF-8 UTF-8 in /etc/locale.gen, then

sudo locale-gen

Any process started after that gets Chinese immediately — pacman output switched in the same shell. Processes already running inside the KDE session don't, so you still need to log out and back in for the desktop itself.

The diagnostic worth memorising: locale printing Cannot set LC_* to default locale: No such file or directory is the confirmed signature of this specific problem — the variable is set, the locale isn't generated. Don't reach for localectl status here. The System Locale it reports comes from /etc/locale.conf, which is the system-level setting; KDE's per-session language lives somewhere else entirely. Reading one while debugging the other will waste your afternoon.

The Korean variant

With Chinese finally rendering, the glyphs were wrong:

sans-serif:lang=zh-cn  -> Noto Sans CJK KR
serif:lang=zh-cn       -> Noto Serif CJK KR
monospace:lang=zh-cn   -> Noto Sans Mono CJK KR

noto-fonts-cjk packs the SC, TC, JP and KR regional variants into a single .ttc. With no fontconfig rules in play, fontconfig picked KR out of the file's internal order. Chinese, Japanese and Korean share Unicode codepoints but not glyph shapes — 直, 骨, 化, 次 all differ — so the text displays perfectly while looking subtly foreign. It reads as "my fonts are broken," which sends people off reinstalling packages that were never the problem.

~/.config/fontconfig/ didn't exist on this machine at all.

I've written up the full fontconfig setup separately — the short version is: declare the priority explicitly, Latin faces first, CJK right behind them. Latin fonts contain no Chinese, so fontconfig falls through to the first CJK entry on its own, which keeps the config readable and the order correct.

sudo pacman -S --needed noto-fonts-extra inter-font
paru -S --needed otf-misans misans-fontconfig maplemono-nf-cn
Two corrections to that earlier post, verified against the actual repos: Inter is inter-font in extra, not ttf-inter. And maplemono-nf-cn is in the AUR, not the official repos. misans-fontconfig turns out to be trivial — it aliases Mi Sans (with a space) to MiSans and nothing else, so it won't fight a hand-written fonts.conf.

Check the real family names before you write any config, or your rules will fail silently:

fc-list -f '%{family[0]}\n' | sort -u | grep -iE '^inter|^misans|^maple'

The alias block, in ~/.config/fontconfig/fonts.conf:

<alias binding="strong">
  <family>sans-serif</family>
  <prefer>
    <family>Inter</family>
    <family>Noto Sans</family>
    <family>MiSans</family>
    <family>Noto Sans CJK SC</family>
    <family>Noto Color Emoji</family>
  </prefer>
</alias>
<!-- serif: Noto Serif -> Noto Serif CJK SC -->
<!-- monospace: Maple Mono NF CN -> Noto Sans Mono CJK SC -->

One rule I added beyond the template, because applications do ask for the unsuffixed family by name and get whatever the bundle hands them:

<match target="pattern">
  <test name="family"><string>Noto Sans CJK</string></test>
  <edit name="family" mode="assign" binding="strong">
    <string>Noto Sans CJK SC</string>
  </edit>
</match>

Verifying:

fc-match "sans-serif:lang=zh-cn"
fc-match -s "sans-serif:lang=zh-cn" | head -5     # the real cascade
fc-match "Noto Sans CJK"                          # the catch-all rule

Generic families now resolve to Inter and Maple Mono NF CN; Chinese characters land on MiSans and Noto Serif CJK SC; the unsuffixed request resolves to SC. No KR anywhere.

A `fc-match` trap: because of binding="strong", fc-match "sans-serif:charset=4e2d" still returns Inter — which contains no Chinese at all. The single-result form tells you what won the match, not what will draw the character. Use fc-match -s and read the sorted list, or go the other way with fc-list ':charset=4e2d'.

Plasma never asks for sans-serif

Here is the part that cost me the most time. After all that fontconfig work, the Plasma UI looked exactly the same.

Plasma's built-in default UI font is Noto Sans — and that is a request by name. It does not go through the sans-serif generic alias, so nothing in the block above applies to it. Inter and MiSans were only being reached as fallback when a character wasn't covered. The desktop itself never asked.

~/.config/kdeglobals had no font entries at all, meaning it was running on those built-in defaults. Plasma 6 stores fonts as a 16-field string:

UI="MiSans,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
SMALL="MiSans,8,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
FIXED="Maple Mono NF CN,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
WMF="MiSans,10,-1,5,700,0,0,0,0,0,0,0,0,0,0,1"

kwriteconfig6 --file kdeglobals --group General --key font                 "$UI"
kwriteconfig6 --file kdeglobals --group General --key menuFont             "$UI"
kwriteconfig6 --file kdeglobals --group General --key toolBarFont          "$UI"
kwriteconfig6 --file kdeglobals --group General --key smallestReadableFont "$SMALL"
kwriteconfig6 --file kdeglobals --group General --key fixed                "$FIXED"
kwriteconfig6 --file kdeglobals --group WM      --key activeFont           "$WMF"

System Settings → Appearance → Fonts does the same thing if you'd rather click. Back up the file first either way.

MiSans lies about its weights

The UI was now in the right language and the right typeface, and everything looked slightly too bold. Checking the actual metadata explained it:

$ fc-list "MiSans" -f '%{file|basename}\t%{style}\tweight=%{weight}\n'
MiSans-Regular.otf   Regular          weight=53
MiSans-Medium.otf    Medium,Regular   weight=75
MiSans-Bold.otf      Bold             weight=186
MiSans-Heavy.otf     Heavy,Regular    weight=200

fontconfig's constants are light=50, demilight=55, book=75, regular=80, medium=100, demibold=180, bold=200, black=210. So MiSans-Regular declares itself at 53 — somewhere between light and demilight — and MiSans-Heavy declares exactly 200, which is fontconfig's bold.

The consequence is systematic. Qt asks for regular (80) and the nearest declared weight is Medium at 75. Qt asks for bold (200) and hits Heavy dead on. Every piece of text in the UI comes out one step heavier than requested.

The fix is a target="scan" rule, which rewrites the property as fontconfig indexes the file:

<match target="scan">
  <test name="file" compare="contains"><string>MiSans-Regular.</string></test>
  <edit name="weight"><const>regular</const></edit>
</match>

One of those per face, mapping Thin→thin, ExtraLight→extralight, Light→light, Normal→demilight, Regular→regular, Medium→medium, Demibold→demibold, Semibold→demibold, Bold→bold, Heavy→black. Rebuild the cache afterwards — target="scan" is only evaluated during the scan, so nothing happens until you do:

fc-cache -f -r

Don't match on style

My first attempt tested style instead of the filename, which seemed like the more semantic choice:

<test name="style"><string>Regular</string></test>

It collapsed all ten weights onto the same value:

MiSans-Thin.otf        weight=80
MiSans-ExtraLight.otf  weight=80
MiSans-Light.otf       weight=80
MiSans-Normal.otf      weight=80
MiSans-Regular.otf     weight=80

MiSans declares style as a multi-valued list — Thin,Regular, Light,Regular, Medium,Regular — so a test for Regular matches nearly every face in the family, and the ten rules then overwrite each other in file order. The last one to match wins.

Filename matching is reliable here because no MiSans filename is a substring of another: MiSans-Light is not contained in MiSans-ExtraLight, and MiSans-Bold is not contained in MiSans-Semibold. Worth confirming for any family you try this on, since compare="contains" will happily match a prefix.

With that in place, all ten map one-to-one:

thin       -> MiSans-Thin.otf         medium    -> MiSans-Medium.otf
extralight -> MiSans-ExtraLight.otf   demibold  -> MiSans-Demibold.otf
light      -> MiSans-Light.otf        bold      -> MiSans-Bold.otf
demilight  -> MiSans-Normal.otf       black     -> MiSans-Heavy.otf
regular    -> MiSans-Regular.otf

What I deliberately left alone

`/etc/locale.conf` stays at `LANG=en_US.UTF-8`. It governs the TTY, the SDDM login screen, and system services running outside a session. Switching it to Chinese gets you boxes and blanks on the console, because the console font has no Chinese glyphs. The graphical session is already handled by KDE's own plasma-localerc, so there's nothing to gain. If you do want the login screen in Chinese, it's sudo localectl set-locale LANG=zh_CN.UTF-8 — just know what you're trading.

Japanese will use Chinese glyph shapes. The config points CJK at the simplified variants across the board, so kana render in MiSans and kanji get SC forms. For a Chinese-first desktop that's the right call; if you typeset Japanese, add lang=ja rules.

Korean falls back to `Noto Sans CJK SC`. Hangul has no regional glyph variants, so this costs nothing.

The change list

/etc/locale.gen                   enable zh_CN.UTF-8 UTF-8, then locale-gen
~/.config/fontconfig/fonts.conf   new: generic priority, Noto CJK catch-all,
                                  MiSans weight correction, rendering options
~/.config/kdeglobals              UI / menu / toolbar / smallest-readable /
                                  fixed / window-title fonts
packages                          noto-fonts-extra inter-font otf-misans
                                  misans-fontconfig maplemono-nf-cn

Log out and back in for all of it to take effect.

Diagnostics

locale                              # does it report Cannot set LC_*?
locale -a                           # which locales actually exist
localectl status                    # system-level; not the same as the session
fc-match "sans-serif:lang=zh-cn"
fc-match -s "sans-serif:lang=zh-cn" | head    # the real cascade
fc-list "MiSans" -f '%{file|basename}\t%{style}\tweight=%{weight}\n'
fc-list ':charset=4e2d' family      # which fonts cover a given character
fc-conflist                         # which config files are loaded, in order
fc-cache -f -r                      # required after changing target="scan"

Reference

No comments yet