Skip to main content
โšก Calmops

Fixing Chinese Characters Displaying as Squares on Non-Chinese Systems

The Problem

thout CJK language packs.

This is a common issue for websites with Chinese content that have international visitors. Many major Chinese websites don’t handle this, resulting in a poor experience for non-Chinese users.

How to reproduce: Install a fresh OS without Chinese language support, then visit a Chinese website. Or check screenshots from non-Chinese users.

Why It Happens

browser encounters a Chinese character and the CSS font-family doesn’t specify a font that contains CJK glyphs, the browser renders a fallback square.

Why Russian works but Chinese doesn’t: Cyrillic characters are included in many common fonts (Arial, Times New Roman, etc.) because they’re part of the Latin Extended character set. Chinese characters require dedicated CJK fonts, which are large (5-15MB) and not bundled with most Western fonts.

Solution 1: Google Fonts (Easiest)

e Noto Sans SC (Simplified Chinese) font with automatic subsetting โ€” it only sends the characters actually used on the page, keeping file sizes manageable.

<!-- In <head> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet">
body {
  font-family: 'Noto Sans SC', Arial, sans-serif;
}

Pros: Simple, automatic subsetting, CDN delivery Cons: Requires internet access, may be blocked in China, adds HTTP request

Solution 2: JavaScript-Based Conditional Loading

Load the Chinese font only on non-Chinese systems to avoid unnecessary downloads for users who already have Chinese fonts:

// global.js โ€” load Chinese font only on non-Chinese systems
(function() {
  var lang = navigator.language || navigator.userLanguage || '';
  var isChinese = lang === 'zh-CN' || lang === 'zh' ||
            lang.startsWith('zh-');

  if (!isChinese) {
    // Dynamically load Google Fonts for Chinese
    var link = document.createElement('link');
    link.rel  = 'stylesheet';
    link.href = 'https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap';
    document.head.appendChild(link);

    // Apply the font
    document.body.style.fontFamily = "Arial, 'Noto Sans SC', sans-serif";
  }
})();
/* global.css โ€” import is handled by JS, just define fallback */
body {
  font-family: Arial, system-ui, sans-serif;
}

ifeng.com/blog/2014/07/chinese_fonts.html)

  1. Falls back to Arial
  2. Uses Noto Sans SC (from Google Fonts) for systems without Chinese fonts
  3. Includes common Chinese system fonts as additional fallbacks

Resources

```
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial,
               'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}

This stack:

  1. Uses the system font on Apple devices (already has Chinese support)
  2. Uses Segoe UI on Win ubuntu:22.04 bash apt install firefox-esr

Open your website

Option 2: Use browser DevTools

Chrome: DevTools โ†’ Rendering โ†’ Emulate CSS media โ†’ prefers-color-scheme

Check Network tab for font requests

Option 3: Temporarily rename/remove Chinese fonts on your system

macOS: Font Book โ†’ disable Chinese fonts

Linux: fc-list :lang=zh (list Chinese fonts)


## Recommended Approach

For most websites with Chinese content:

```html
<!-- Simple and effective: Google Fonts with unicode-range -->
<link relnts are large because the Unicode CJK block contains ~20,000+ characters:

| Font | Size |
|---|---|
| Arial (Latin only) | ~350 KB |
| Noto Sans SC (full) | ~8-15 MB |
| Noto Sans SC (subsetted, common chars) | ~500 KB - 2 MB |
| Google Fonts (auto-subsetted per page) | ~50-200 KB per page |

Google Fonts' automatic subsetting is why it's the most practical solution for most websites.

## Testing Your Fix

```bash
# Test on a system without Chinese fonts
# Option 1: Use a fresh Docker container
docker run -itrnally. When you include Noto Sans SC via Google Fonts, it only downloads the font subsets that contain characters actually present on the page:

```css
/* Google Fonts handles subsetting automatically */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap');

body {
  /* Noto Sans SC only downloads if Chinese chars are present */
  font-family: Arial, 'Noto Sans SC', sans-serif;
}

This is actually the simplest correct solution for most cases.

Font Size Considerations

Chinese fole: normal; font-weight: 400; font-display: swap; src: url(’/fonts/NotoSansSC-subset.woff2’) format(‘woff2’); unicode-range: U+4E00-9FFF, U+3400-4DBF, U+F900-FAFF; }

body { font-family: Arial, ‘Noto Sans SC’, sans-serif; }


The `unicode-range` descriptor tells the browser to only download this font when the page actually contains characters in that range โ€” so English-only pages don't download it at all.

## Solution 4: CSS unicode-range with Google Fonts

Google Fonts already uses `unicode-range` inte

### Step 1: Generate Subsetted Font Files

Use [google-webfonts-helper](https://gwfh.mranftl.com/fonts/noto-sans-sc) to download and subset the font:

```bash
# Install fonttools for subsetting
pip install fonttools brotli

# Subset to only the characters you need
pyftsubset NotoSansSC-Regular.ttf \
  --unicodes="U+4E00-9FFF,U+3400-4DBF,U+20000-2A6DF" \
  --flavor=woff2 \
  --output-file=NotoSansSC-subset.woff2

Step 2: Host and Reference

@font-face {
  font-family: 'Noto Sans SC';
  font-sty
**Why not just use CSS `font-family` with Noto Sans SC?**

Setting `font-family: Arial, 'Noto Sans SC', sans-serif` in CSS causes the browser to always request the Noto Sans SC font file โ€” even on Chinese systems that don't need it. This causes unnecessary network requests and potential flash of unstyled text (FOUT) on Chinese systems.

## Solution 3: Self-Hosted Subsetted Font

For better performance and no dependency on Google's CDN, host the font yourself with subsetting:

Comments