Skip to content

fix(mobile): pin CJK fonts so iOS WKWebView doesn't render ? boxes#92

Merged
attson merged 1 commit into
mainfrom
fix/mobile-cjk-fonts
Jun 2, 2026
Merged

fix(mobile): pin CJK fonts so iOS WKWebView doesn't render ? boxes#92
attson merged 1 commit into
mainfrom
fix/mobile-cjk-fonts

Conversation

@attson
Copy link
Copy Markdown
Owner

@attson attson commented Jun 1, 2026

Repro

On iOS 26 Capacitor build (verified iPhone 17 Pro / iOS 26.3 simulator), all Chinese-locale i18n strings render as missing-glyph `[?]` boxes — `mobile.relayDisconnected` = "Relay 已断开" shows as "Relay [?] [?] [?]", same for `mobile.setupSubtitle`, all the insecure-HTTP warning text, etc. English text renders fine.

Root cause

iOS 26 WebKit regression. `-apple-system` (and `system-ui`) declares full Unicode coverage but doesn't actually render CJK glyphs, AND WebKit refuses to fall through to later families in the font-family list. Stacks that start with `-apple-system, ..., "PingFang SC", ...` fail every CJK glyph even with PingFang SC explicitly in the list.

Verified via a font-probe page hitting the live WKWebView:

stack CJK render
`sans-serif` alone
`-apple-system` alone
`system-ui` alone
`"PingFang SC"` direct
`"Heiti SC"` direct
`-apple-system, ..., "PingFang SC", ...` (the obvious "fix")
`"PingFang SC", ..., Helvetica, sans-serif` (this PR)

`document.fonts.check` reports all CJK families as present — so the fonts are installed, WebKit just won't reach them when `-apple-system` sits in front.

Fix

  • New CSS vars `--font-sans` / `--font-mono` at `:root` in `src/style.css` with PingFang SC first, then Hiragino Sans GB / Heiti SC / Microsoft YaHei / Noto Sans CJK SC, then Helvetica Neue / Helvetica / Arial / sans-serif. `-apple-system` and `system-ui` are intentionally dropped — too risky given the iOS 26 bug, and Helvetica Neue is iOS's native Latin fallback already.
  • Inline `<style>` in `index.capacitor.html` with the same stack so WKWebView's parse-time font-selection is correct before the Vue bundle's style.css loads.
  • `` (was `"en"`) — i18n already syncs this on locale change but the parse-time value matters for WKWebView.
  • All mobile component scoped styles (`MobileSetup`, `MobileSessionList`, `MobileTerminal`, `PairingConsume`) now reference `var(--font-sans|mono)` instead of repeating their own font stacks — future components inherit the fix automatically.

Test plan

  • `npm test` — 644 tests green
  • `npx vue-tsc --noEmit` — clean
  • `npm run build:capacitor` — clean
  • Font probe: row H ("PingFang SC first" stack) renders Chinese + English correctly on iOS 26.3 simulator
  • manual: `cd mobile && npm run ios:sync && cap open ios`, Clean Build Folder, Run — confirm MobileSetup / MobileSessionList / MobileTerminal Chinese text renders.

Out of scope

  • iOS < 26 / macOS Wails are not affected by the `-apple-system` regression; this fix only makes the desktop stacks slightly less Apple-native (PingFang SC's Latin glyphs are used instead of SF). Acceptable since the same bundle ships everywhere.
  • Bundling a webfont (e.g. Noto Sans SC subset) was avoided — adds ~300KB-2MB and isn't needed once the system fonts are reachable.

iOS 26 WebKit regresses `-apple-system` / `system-ui`: the composite
font reports full Unicode coverage but doesn't actually render CJK
glyphs, AND WebKit refuses to fall through to later families in the
font-family list. Stacks that start with `-apple-system, ..., "PingFang
SC", ...` show every CJK character as a missing-glyph box even though
PingFang SC is present and works when referenced directly.

Verified via tmp-font-probe.html on iPhone 17 Pro / iOS 26.3 simulator:
- `-apple-system` alone → ? boxes
- `system-ui` alone   → ? boxes
- `"PingFang SC"` direct → renders fine
- `document.fonts.check` reports all CJK families OK

Workaround:
- Put PingFang SC FIRST in --font-sans / --font-mono (CSS vars in
  style.css) so per-glyph fallback finds it before WebKit short-circuits
  on the broken -apple-system entry.
- Drop -apple-system / BlinkMacSystemFont from the stack — too risky
  given the iOS 26 bug; Helvetica Neue is iOS's normal Latin fallback.
- Inline the same stack in index.capacitor.html so WKWebView's
  parse-time font-selection has the correct chain before style.css
  loads.
- Component-level styles (MobileSetup, MobileSessionList, MobileTerminal,
  PairingConsume) all reference var(--font-sans|mono) so future
  components inherit the fix automatically.
@attson attson force-pushed the fix/mobile-cjk-fonts branch from ad82de1 to e066ddf Compare June 2, 2026 14:15
@attson attson merged commit 4914363 into main Jun 2, 2026
6 checks passed
@attson attson deleted the fix/mobile-cjk-fonts branch June 2, 2026 14:17
attson added a commit that referenced this pull request Jun 2, 2026
Follow-up to #92. Three leftover [?] boxes on iOS 26 MobileSetup:

1. Language <select> showed "跟随系统" as four boxes. The control
   inherited --font-mono which had ui-monospace first — same iOS 26
   short-circuit bug as -apple-system.
2. The "⚠" warning icon in the insecure-mode hint rendered as a box
   because no font in our stack covers U+26A0 in text presentation,
   and we never explicitly requested emoji presentation.
3. Same ⚠ in InsecureBanner header.

Changes:
- Reorder --font-mono to put PingFang SC first (mirrors --font-sans).
- Add --font-mono-strict for ASCII-only URL/token inputs that want a
  real monospace look.
- MobileSetup: <input> on mono-strict, <select> on sans.
- Add "Apple Color Emoji" to --font-sans + index.capacitor.html.
- Append U+FE0F (variation selector-16) to the two ⚠ icons so iOS
  picks the emoji glyph rather than missing text-style.
attson added a commit that referenced this pull request Jun 2, 2026
Follow-up to #92. Two leftover [?] boxes on iOS 26 MobileSetup:

1. Language <select> showed "跟随系统" as four boxes. The control
   inherited --font-mono which had ui-monospace first — same iOS 26
   short-circuit-on-claim bug as -apple-system.
2. The "⚠" warning icon rendered as a box, in both MobileSetup's
   warn-hint and InsecureBanner's header. Adding U+FE0F (variation
   selector-16) to request emoji presentation did not help — iOS 26
   simulator's Apple Color Emoji still produced a missing-glyph box.

Changes:
- Reorder --font-mono to put PingFang SC first (mirrors --font-sans).
- Add --font-mono-strict for ASCII-only URL/token inputs that want a
  real monospace look.
- MobileSetup: <input> uses mono-strict, <select> uses sans.
- Add "Apple Color Emoji" to --font-sans + index.capacitor.html
  inline stack — helps in environments where emoji rendering works.
- Replace the U+26A0 emoji in MobileSetup's warn-hint and the
  InsecureBanner head with a lucide-style inline SVG. Removes the
  font-dependency entirely; icon now renders on any platform.
attson added a commit that referenced this pull request Jun 2, 2026
Follow-up to #92. Two leftover [?] boxes on iOS 26 MobileSetup:

1. Language <select> showed "跟随系统" as four boxes. The control
   inherited --font-mono which had ui-monospace first — same iOS 26
   short-circuit-on-claim bug as -apple-system.
2. The "⚠" warning icon rendered as a box, in both MobileSetup's
   warn-hint and InsecureBanner's header. Adding U+FE0F (variation
   selector-16) to request emoji presentation did not help — iOS 26
   simulator's Apple Color Emoji still produced a missing-glyph box.

Changes:
- Reorder --font-mono to put PingFang SC first (mirrors --font-sans).
- Add --font-mono-strict for ASCII-only URL/token inputs that want a
  real monospace look.
- MobileSetup: <input> uses mono-strict, <select> uses sans.
- Add "Apple Color Emoji" to --font-sans + index.capacitor.html
  inline stack — helps in environments where emoji rendering works.
- Replace the U+26A0 emoji in MobileSetup's warn-hint and the
  InsecureBanner head with a lucide-style inline SVG. Removes the
  font-dependency entirely; icon now renders on any platform.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant