← 返回首页

Ubuntu 26.04 中文显示成日文字形的修复(Fontconfig 排错实录)

2026-05-26 · LinuxFontconfigUbuntu 26.04

环境: Ubuntu 26.04,英文 locale(LANG=en_US.UTF-8)、GNOME 50、默认 Wayland。

系统语言保持英文(LANG=en_US.UTF-8),但希望中文按简体(SC)字形显示。结果界面里部分汉字看着很别扭——字形偏窄、左右留白偏多(典型如「复」字),可占位宽度又一样。这不是排版宽度问题,是字形本身选错了。本文记录从取证到根因再到最终修复的完整过程,不改系统语言、不影响英文。

一条关键线索:正文对,地址栏错

仔细观察会发现:网页正文里的中文是正常的,而浏览器地址栏、应用 UI、终端里的中文不正常。同一个字,在不同地方长得不一样——说明正文和 UI 走的是两条不同的字体选择路径。

根因

系统装的 Noto Sans CJK 是一个合并字库,含 JP / KR / SC / TC / HK 五个地区字形。Ubuntu 的 /etc/fonts/conf.d/64-language-selector-cjk-prefer.conf 给通用字族(sans-serif / serif / monospace)排的 CJK 顺序是:

Noto Sans CJK JP   ← 排第一
Noto Sans CJK KR
Noto Sans CJK SC   ← 简体排第三
Noto Sans CJK TC
Noto Sans CJK HK

这些 CJK 字体是用 <prefer> 插在拉丁主字体(Noto Sans)之后的,所以英文一直正常;但当文本**没有语言标记(no lang hint)**时,汉字会落到排在最前的 JP,于是共享汉字显示成了日文字形。

这就解释了"正文对、UI 错":浏览器渲染正文时会按内容把语言标成 zh-cn,命中简体;而地址栏、应用 UI、终端这些路径不带 lang 提示,在英文 locale 下没有任何中文倾向,默认就吃了排第一的 JP。

取证:别猜,要证据

fc-match 指定单个码位,直接看系统给「复」(U+590D)挑哪个字体:

# 无语言提示(地址栏 / UI 走的路径)
$ fc-match 'sans-serif:charset=590d'
NotoSansCJK-Regular.ttc: "Noto Sans CJK JP" "Regular"   # ← 日文!

# 带 zh-cn(网页正文走的路径)
$ fc-match 'sans-serif:charset=590d:lang=zh-cn'
NotoSansCJK-Regular.ttc: "Noto Sans CJK SC" "Regular"   # ← 简体,正常

终端类程序(如 Ghostty)可以用它自带的工具确认每个码位实际用了哪个 face:

$ ghostty +show-face --string="复习abc"
U+590D « 复 » found in face "Noto Sans Mono CJK JP".   # 修复前是 JP

走过的弯路

下面几种直觉做法都不行,值得记下来避免重复踩:

教训:fontconfig 里 prepend / prefer / append 的最终排序受加载顺序和 binding(weak / strong)共同影响,不要靠猜,改完必须用 fc-match 验证

解决方案

确定性写法:为每个通用字族用**强绑定(binding="strong")**显式前插一个 [拉丁主字体, 然后 SC] 的头部。拉丁字体在前,英文 / 希腊 / 西里尔仍用它;只有它不含的汉字才落到紧随其后的 SC;而 SC 因为是强绑定,排在系统那条弱绑定的 JP 之前。

新建 ~/.config/fontconfig/conf.d/80-prefer-sc-default.conf:

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
  <match target="pattern">
    <test name="family"><string>sans-serif</string></test>
    <edit name="family" mode="prepend" binding="strong">
      <string>Noto Sans</string>
      <string>Noto Sans CJK SC</string>
    </edit>
  </match>
  <match target="pattern">
    <test name="family"><string>serif</string></test>
    <edit name="family" mode="prepend" binding="strong">
      <string>Noto Serif</string>
      <string>Noto Serif CJK SC</string>
    </edit>
  </match>
  <match target="pattern">
    <test name="family"><string>monospace</string></test>
    <edit name="family" mode="prepend" binding="strong">
      <string>DejaVu Sans Mono</string>
      <string>Noto Sans Mono CJK SC</string>
    </edit>
  </match>
</fontconfig>

然后刷新缓存:

fc-cache -f

几个要点:文件名前缀 80 让它在系统的 64-... 之后加载;把拉丁字体(Noto Sans / Noto Serif / DejaVu Sans Mono)写在第一位,是保证英文不被污染的关键;如果你的默认等宽不是 DejaVu,按需替换。

验证

# 目标:无语言提示的汉字 → SC
fc-match 'sans-serif:charset=590d'     # 期望 Noto Sans CJK SC
fc-match 'monospace:charset=590d'      # 期望 Noto Sans Mono CJK SC

# 安全:英文 / 拉丁一字不动
fc-match 'sans-serif'                   # 期望 Noto Sans
fc-match 'monospace'                    # 期望 DejaVu Sans Mono
fc-match 'sans-serif:charset=41'        # 字母 A,期望 Noto Sans

让各程序生效

fontconfig 在程序启动时读取,改完必须彻底重启对应程序,刷新页面或重开标签页都没用:

副作用与权衡

本方案用强绑定压过了"按语言指定字形"的规则,代价是:lang=ja(日文)和 lang=zh-tw(繁体)的网页,其共享汉字也会显示成简体字形。对纯简体用户通常无所谓;若需要保留日文 / 繁体字形,可以再补按语言的强规则单独覆盖,并同样用 fc-match 逐一验证。

撤销

rm ~/.config/fontconfig/conf.d/80-prefer-sc-default.conf && fc-cache -f

小结

这个问题的本质,是英文 locale 下"无语言提示的汉字"被默认匹配到了 Noto CJK 的日文字形。排查的关键不是凭经验下结论,而是用 fc-match 把"系统到底给这个字挑了谁"摆出来;修复的关键是认清 fontconfig 的排序由 binding 强弱和加载顺序决定,用确定性的强绑定显式前插,并且每改一次都验证"中文修好"且"英文没坏"。