fix(web): add self-hosted Noto Sans Cyrillic fallback after Albert Sans - #6498
fix(web): add self-hosted Noto Sans Cyrillic fallback after Albert Sans#6498Sid-Qin wants to merge 3 commits into
Conversation
Albert Sans has no Russian glyphs, so ru UI fell through to OS fonts. Vendor four unicode-range-limited Noto Sans variable WOFF2 subsets (~198 KB, OFL-1.1) and place Noto Sans immediately after Albert Sans in --serif/--sans so Latin stays Albert and Cyrillic is deterministic offline. Also recover the Noto faces on od:// via font-recovery and add regression coverage for assets, token order, and Russian alphabet unicode-range coverage. Fixes nexu-io#6478
|
Thanks @Sid-Qin — keeping Albert Sans first while adding a bundled Cyrillic fallback for #6478 looks like the right direction. I'm routing the PR now and will follow up once the required checks are in place.
|
|
🧪 This PR has changes that need a manual QA pass before merge — please hold off self-merging for now; we'll loop QA in once it's merge-ready (and design/product have signed off, where applicable). |
lefarcen
left a comment
There was a problem hiding this comment.
Hey @Sid-Qin, the overall PR write-up is already strong, especially the why and the surface-area callout. One small template nudge before pool review: could you add a short prose validation summary alongside the command block so the Validation section reads as complete to the template checker?
nettee
left a comment
There was a problem hiding this comment.
@Sid-Qin, the vendored subsets, Unicode ranges, token ordering, focused tests, and production build all look good. I found one packaged-mode recovery gap in the new Noto wiring; the blocking detail is inline.
🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.| }, | ||
| // Cyrillic-only fallback faces (#6478). Same od:// recovery path as Albert. | ||
| { | ||
| family: 'Noto Sans', |
There was a problem hiding this comment.
Blocking: recover Noto faces when they fail after startup
These new faces are only recovered when installFontRecovery sees their family in status === 'error', but that function samples doc.fonts only at 0/4/15/45 seconds. Because the Noto faces are Cyrillic-range-limited, an app that starts in English leaves them unloaded; switching to ru after the final sweep is the first request, and Chromium then changes the face to error. No sweep runs after that, so packaged od:// sessions still fall through to the OS CJK/generic fonts—the exact fallback this PR is meant to remove. I reproduced this in Electron: Albert stayed loaded, Noto stayed unloaded for Latin text, then became error when the body changed to Russian.
Subscribe to doc.fonts loadingerror and trigger sweep() (removing the listener in the returned cancel function), or invoke a recovery sweep from the locale-change path. Add a regression test that starts with an unloaded Noto face, advances past the scheduled timers, then triggers the later load error and asserts the four JS faces are registered.
🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.There was a problem hiding this comment.
Fixed in the follow-up commit.
installFontRecovery now subscribes to doc.fonts loadingerror (removed on cancel) so a late Cyrillic-range load failure after the timed sweeps still runs recovery. Added a regression test that drains the startup timers with Noto still unloaded, then flips the face to error and dispatches loadingerror, asserting all four JS faces are registered.
|
Hey @Sid-Qin — nettee's packaged-mode recovery note is the main blocker on the current head, and it lines up with the new |
Cyrillic-range Noto faces often stay unloaded until the first Russian glyph request. On packaged od:// that late load can error after the 0/4/15/45s recovery timers finished, so Russian text still fell through to OS fonts. Subscribe to FontFaceSet loadingerror to re-run recovery sweeps after locale switches, remove the listener on cancel, and cover the late-error path with a regression test. Addresses review on nexu-io#6498 / nexu-io#6478.
|
Addressed the packaged-mode recovery gap from @nettee's review. Fix
Test
Also expanded the PR Validation section with a short prose summary alongside the command block. |
nettee
left a comment
There was a problem hiding this comment.
This head cleanly adds the bundled Cyrillic Noto Sans fallback while preserving Albert Sans for Latin text, including the late packaged loadingerror recovery path. I verified the focused regression suite (14/14), the full web suite (598 files, 6,226 tests with 11 skipped), web typecheck, production build, repository guard, and workspace typecheck all pass.
Thanks for the careful follow-up on the packaged-mode review feedback—the recovery listener and cancellation coverage make this a solid fix for the locale-switch case.
🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.|
@Sid-Qin — nettee's approval is in on the current head, good work on the packaged-mode recovery fix. Two CI checks failed on Worth investigating those before design + QA can complete their sign-off. |
|
Re-triggered CI with an empty commit. The prior red checks were:
No product code change in this push; waiting for a clean re-run so design/QA can continue. |
|
Hey @Sid-Qin — the current blocker is the refresh requested above: rebase this onto the current |


































































Fixes #6478
Why
After #6142, both
--serifand--sansstart with Albert Sans, but the bundled Albert Sans variable TTF has no basic Russian Cyrillic glyphs (0/66 Russian letters). Russian UI text therefore falls through toPingFang SC/Microsoft YaHei/ genericsans-serifand becomes OS-dependent, while Latin stays Albert Sans.This is intentionally not a re-open of #6085 / #6089 (Source Serif). Maintainer triage on #6478 agreed: keep Albert Sans first and add a same-origin, Cyrillic-capable fallback.
Issue author recommended vendoring four Noto Sans variable WOFF2 subsets from
@fontsource-variable/noto-sans@5.3.0(~198 KB) with OFL metadata rather than adding a Fontsource runtime dependency — this PR implements that shape.What users will see
od://sessions recover Albert and Noto faces via font-recovery, including late Cyrillic loads after an English startup / locale switch.Surface area
What changed
apps/web/public/fonts/noto-sans-cyrillic*.woff2apps/web/public/fonts/OFL-NotoSans.txtapps/web/public/fonts/SOURCE-NotoSansCyrillic.mdapps/web/src/styles/base.css@font-facefor Noto Sans + Cyrillic/Cyrillic-ext rangesapps/web/src/styles/tokens.css--serif/--sans: Albert → Noto Sans → CJK → genericapps/web/src/runtime/font-recovery.tsloadingerrorfor late range-limited failuresValidation
Unit coverage confirms the stacked tokens keep Albert Sans first, the four vendored WOFF2 subsets declare Cyrillic unicode-ranges that cover all 66 basic Russian letters, and font-recovery registers Noto faces both on the timed startup sweeps and on a late
loadingerrorafter those timers have finished (English → Russian / packagedod://path). No Fontsource npm dependency is introduced.Bug fix verification
apps/web/tests/styles/cyrillic-font-fallback.test.tsapps/web/tests/runtime/font-recovery.test.tsloadingerror)Risks / notes
needs-validation/needs-design-reviewlabels (rulocale narrow + desktop).Screenshots
N/A in this environment. Suggest verifying
rulocale headings/body at narrow + desktop viewports, including switching locale after English cold start on packagedod://.