From f3b236ae820bf9f637b9995a7777ad34b2e3b9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 12 Oct 2026 00:11:53 -0400 Subject: [PATCH] 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. --- default/fontconfig/conf.avail/50-omarchy.conf | 292 ++++++++++++++++++ test/shell.d/fontconfig-cjk-test.sh | 133 ++++++++ 2 files changed, 425 insertions(+) create mode 100755 test/shell.d/fontconfig-cjk-test.sh diff --git a/default/fontconfig/conf.avail/50-omarchy.conf b/default/fontconfig/conf.avail/50-omarchy.conf index eecf66e0..78ba9d95 100644 --- a/default/fontconfig/conf.avail/50-omarchy.conf +++ b/default/fontconfig/conf.avail/50-omarchy.conf @@ -1,6 +1,298 @@ + + + + + zh + + + zh-cn + + + + + + zh-hant + + + zh-tw + + + + + + zh-hant-hk + + + zh-hk + + + + + + zh-hant-mo + + + zh-hk + + + + + + zh-hant + + + zh-tw + + + + + + zh-hans + + + zh-cn + + + + + + zh-sg + + + zh-cn + + + + + + zh-mo + + + zh-hk + + + + + + yue + + + zh-hk + + + + + + yue-hans + + + zh-cn + + + + + + yue-cn + + + zh-cn + + + + + + yue + + + zh-hk + + + + + + + + zh-cn + + + sans-serif + + + Noto Sans CJK SC + + + + + + zh-cn + + + serif + + + Noto Serif CJK SC + + + + + + zh-cn + + + monospace + + + Noto Sans Mono CJK SC + + + + + + zh-hk + + + sans-serif + + + Noto Sans CJK HK + + + + + + zh-hk + + + serif + + + Noto Serif CJK HK + + + + + + zh-hk + + + monospace + + + Noto Sans Mono CJK HK + + + + + + zh-tw + + + sans-serif + + + Noto Sans CJK TC + + + + + + zh-tw + + + serif + + + Noto Serif CJK TC + + + + + + zh-tw + + + monospace + + + Noto Sans Mono CJK TC + + + + + + ja + + + sans-serif + + + Noto Sans CJK JP + + + + + + ja + + + serif + + + Noto Serif CJK JP + + + + + + ja + + + monospace + + + Noto Sans Mono CJK JP + + + + + + ko + + + sans-serif + + + Noto Sans CJK KR + + + + + + ko + + + serif + + + Noto Serif CJK KR + + + + + + ko + + + monospace + + + Noto Sans Mono CJK KR + + + sans-serif diff --git a/test/shell.d/fontconfig-cjk-test.sh b/test/shell.d/fontconfig-cjk-test.sh new file mode 100755 index 00000000..3899d78b --- /dev/null +++ b/test/shell.d/fontconfig-cjk-test.sh @@ -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 '' + echo "/usr/share/fonts$test_tmp/cache" + 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 "$candidate" + placed=true + fi + echo "$conf" + done + $placed || echo "$candidate" + echo '' +} >"$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 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"