Pick the matching Noto CJK variant for language-tagged text

The generic assigns swap sans-serif, serif and monospace for fonts
with no CJK glyphs, so tagged Chinese text ends up on whatever Noto
CJK variant the charset scan finds first, usually the Japanese one.
Fold the many Chinese tags down to one per variant and prepend the
matching family ahead of the assigns.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 00:11:53 -04:00
1 parent 9480d1b6a9
commit f3b236ae82
2 files changed
+425

No files matched your search

@@ -1,6 +1,298 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- CJK: fold the language tag onto one tag per Noto CJK variant -->
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-hk</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-sg</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-cn</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<!-- Then append that variant after the generic family it falls back from -->
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="family" qual="any">
<string>sans-serif</string>
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
if ! command -v fc-match >/dev/null || [[ $(fc-list : file) != *NotoSansCJK-Regular.ttc* ]]; then
skip "fontconfig CJK variants need fc-match and noto-fonts-cjk"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
candidate="$ROOT/default/fontconfig/conf.avail/50-omarchy.conf"
{
echo '<?xml version="1.0"?><!DOCTYPE fontconfig SYSTEM "fonts.dtd"><fontconfig>'
echo "<dir>/usr/share/fonts</dir><cachedir>$test_tmp/cache</cachedir>"
placed=false
for conf in /etc/fonts/conf.d/*.conf; do
name=${conf##*/}
[[ $name == "50-omarchy.conf" ]] && continue
if ! $placed && [[ $name > "50-omarchy.conf" ]]; then
echo "<include>$candidate</include>"
placed=true
fi
echo "<include>$conf</include>"
done
$placed || echo "<include>$candidate</include>"
echo '</fontconfig>'
} >"$test_tmp/fonts.conf"
export FONTCONFIG_FILE="$test_tmp/fonts.conf"
fc-cache >/dev/null
# Pango, Qt and Skia add lang as a string, which fc-match never does.
cat >"$test_tmp/string-lang.py" <<'PY'
import ctypes, sys
fc = ctypes.CDLL("libfontconfig.so.1")
fc.FcPatternCreate.restype = ctypes.c_void_p
fc.FcPatternAddString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
fc.FcConfigSubstitute.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int]
fc.FcDefaultSubstitute.argtypes = [ctypes.c_void_p]
fc.FcPatternFormat.restype = ctypes.c_char_p
fc.FcPatternFormat.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
fc.FcCharSetCreate.restype = ctypes.c_void_p
fc.FcCharSetAddChar.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
fc.FcPatternAddCharSet.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p]
fc.FcFontMatch.restype = ctypes.c_void_p
fc.FcFontMatch.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
for line in sys.stdin:
family, lang, char = line.split()
pattern = fc.FcPatternCreate()
fc.FcPatternAddString(pattern, b"family", family.encode())
if lang != "-":
fc.FcPatternAddString(pattern, b"lang", lang.encode())
if char == "families":
fc.FcConfigSubstitute(None, pattern, 0)
print(fc.FcPatternFormat(pattern, b"%{family}").decode())
continue
if char != "-":
charset = fc.FcCharSetCreate()
fc.FcCharSetAddChar(charset, int(char, 16))
fc.FcPatternAddCharSet(pattern, b"charset", charset)
fc.FcConfigSubstitute(None, pattern, 0)
fc.FcDefaultSubstitute(pattern)
result = ctypes.c_int()
print(fc.FcPatternFormat(fc.FcFontMatch(None, pattern, ctypes.byref(result)), b"%{family[0]}").decode())
PY
# resolve <locale> <string|langset> reads "family lang char" lines
resolve() {
if [[ $2 == "string" ]]; then
LANG=$1 python3 "$test_tmp/string-lang.py"
else
local family lang char query
while read -r family lang char; do
query=$family
[[ $lang == "-" ]] || query+=":lang=$lang"
[[ $char == "-" ]] || query+=":charset=$char"
LANG=$1 fc-match -f '%{family[0]}\n' "$query"
done
fi
}
declare -A latin=([sans-serif]="Liberation Sans" [serif]="Liberation Serif" [monospace]="JetBrainsMono Nerd Font")
declare -A cjk=([sans-serif]="Noto Sans CJK" [serif]="Noto Serif CJK" [monospace]="Noto Sans Mono CJK")
declare -A variant=(
[zh]=SC [zh-cn]=SC [zh-sg]=SC [zh-hans]=SC [zh-hans-cn]=SC [zh-hans-hk]=SC
[zh-tw]=TC [zh-hant]=TC [zh-hant-tw]=TC
[zh-hk]=HK [zh-mo]=HK [zh-hant-hk]=HK [zh-hant-mo]=HK
[yue]=HK [yue-hk]=HK [yue-hant]=HK [yue-hant-hk]=HK [yue-hant-cn]=HK
[yue-hans]=SC [yue-hans-cn]=SC [yue-hans-hk]=SC [yue-cn]=SC
[ja]=JP [ja-jp]=JP [ko]=KR [ko-kr]=KR
)
queries=() expected=()
for tag in "${!variant[@]}"; do
for generic in sans-serif serif monospace; do
queries+=("$generic $tag -") expected+=("${latin[$generic]}")
queries+=("$generic $tag 9aa8") expected+=("${cjk[$generic]} ${variant[$tag]}")
done
done
for generic in sans-serif serif monospace; do
queries+=("$generic - -") expected+=("${latin[$generic]}")
done
for locale in C.UTF-8 zh_CN.UTF-8; do
for path in string langset; do
mapfile -t actual < <(printf '%s\n' "${queries[@]}" | resolve "$locale" "$path")
for i in "${!queries[@]}"; do
[[ ${actual[i]} == "${expected[i]}" ]] ||
fail "$locale $path lang: ${queries[i]} resolves to ${expected[i]}" "${actual[i]}"
done
done
pass "$locale: tagged CJK keeps Omarchy's Latin families and sets Han in the tag's variant"
done
for path in string langset; do
for generic in sans-serif serif monospace; do
actual=$(echo "$generic - 9aa8" | resolve zh_CN.UTF-8 "$path")
[[ $actual == "${cjk[$generic]} SC" ]] || fail "zh_CN $path: untagged Han in $generic is SC" "$actual"
done
done
pass "a Chinese locale sets untagged Han in SC"
untouched=$(echo "sans-serif en families" | resolve C.UTF-8 string)
[[ $(echo "sans-serif ja families" | resolve C.UTF-8 string) != "$untouched" ]] ||
fail "lang=ja changes the family list, so the prefix check below can see a match"
for tag in jam jav kok koi; do
actual=$(echo "sans-serif $tag families" | resolve C.UTF-8 string)
[[ $actual == "$untouched" ]] || fail "string lang=$tag is not taken for Japanese or Korean" "$actual"
done
pass "languages that only share a prefix with ja or ko get no CJK variant"