中文排版与国际化设计规范

CJK 排版、竖排文字与多语言界面设计


1. 中文排版的独特性

中文排版与西文排版有本质差异。西文是"词"为基本单位,用空格分隔,字母等宽或变宽混合;中文是"字"为基本单位,无空格分隔,每个字符占据一个正方形的"字身"。这些差异深刻影响了换行、对齐、间距和字体选择。

西文排版:
The quick brown fox jumps.
|   |     |     |   |     |
[词] [词]  [词]  [词] [词]  <- 以词为单位,空格分隔

中文排版:
这是一段中文排版示例文字。
| | | | | | | | | | | | |
[字][字][字]...[字][字][字] <- 以字为单位,无分隔

1.1 关键术语

术语 定义 设计影响
字身 每个字符占据的虚拟方块 字号 = 字身宽度 = 字身高度
避头尾 特定标点不出现在行首/行尾 需要换行算法支持
全角/半角 全角占一个字身,半角占半个 中英混排时需处理
直排/横排 文字方向:纵向/横向 CSS writing-mode
标点悬挂 行尾标点可悬挂在版心外 hanging-punctuation

2. 字体选择

2.1 中文字体分类

类别 特征 适用场景 代表字体
黑体 笔画粗细均匀,无衬线 标题、UI 界面 思源黑体、苹方
宋体 横细竖粗,有衬线 正文、印刷 思源宋体、方正书宋
楷体 模仿手写,笔画有起收 引用、文学 楷体、方正楷体
圆体 笔画末端圆润 儿童、轻松场景 苹方-简 粗、圆体
等宽 所有字符等宽 代码显示 Sarasa Mono SC

2.2 中文 Web 字体策略

/* 推荐的中文字体栈 */

/* 方案 A: 系统字体优先(加载快,体验一致) */
.font-system-zh {
  font-family:
    -apple-system,          /* macOS/iOS 苹方 */
    "PingFang SC",          /* macOS 苹方(显式声明) */
    "Microsoft YaHei",      /* Windows 微软雅黑 */
    "Noto Sans SC",         /* Android/Linux */
    "Source Han Sans SC",   /* 跨平台开源 */
    sans-serif;
}

/* 方案 B: Web Font(品牌一致性,加载较慢) */
@font-face {
  font-family: "Source Han Sans SC";
  src: url("/fonts/SourceHanSansSC-Regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;  /* 先显示后替换,避免 FOIT */
  unicode-range: U+4E00-9FFF, U+3400-4DBF;  /* 只加载中文字符 */
}

/* 方案 C: 子集化(仅加载页面用到的字符) */
/* 使用 fonttools 的 pyftsubset 工具 */
/* pyftsubset font.otf --text-file=chars.txt --output-file=subset.woff2 */

2.3 字体文件大小

字体 全量 常用 6000 字子集 格式
思源黑体 Regular 8.5 MB ~1.2 MB OTF
思源黑体 Regular 4.2 MB ~600 KB WOFF2
Noto Sans SC Regular 8.8 MB ~1.3 MB OTF
Noto Sans SC Regular 4.5 MB ~650 KB WOFF2

3. 中文排版 CSS 规范

3.1 基础排版

/* 中文正文排版基础 */
.zh-body {
  /* 字体 */
  font-family: "Source Han Sans SC", "PingFang SC", sans-serif;
  font-size: 16px;
  font-weight: 400;

  /* 行高: 中文推荐 1.6-1.8 倍(高于西文的 1.4-1.5) */
  line-height: 1.75;

  /* 段落间距 */
  margin-bottom: 1em;

  /* 对齐: 两端对齐 */
  text-align: justify;
  text-justify: inter-ideograph;  /* CJK 字间调整 */

  /* 换行控制 */
  word-break: normal;
  overflow-wrap: break-word;
  line-break: strict;  /* 严格避头尾规则 */

  /* 字间距: 中文一般不需要额外字间距 */
  letter-spacing: 0;

  /* 标点处理 */
  hanging-punctuation: allow-end;  /* 行尾标点悬挂 */

  /* 颜色: 中文正文推荐深灰而非纯黑 */
  color: #333333;
}

/* 标题 */
.zh-heading {
  font-weight: 700;       /* 中文加粗需要 700 */
  line-height: 1.3;       /* 标题行高较紧 */
  letter-spacing: 0.02em; /* 标题可微调字间距 */
}

3.2 避头尾规则

不可出现在行首的字符(避头):

,。、;:!?)》」』】〉》)

不可出现在行尾的字符(避尾):

(《「『【〈《(
/* CSS 原生避头尾 */
.zh-text {
  line-break: strict;
  /* strict 模式下,浏览器自动处理 CJK 避头尾 */
}

3.3 中英文混排

/* 中英文混排间距 */
.zh-mixed {
  /* 方案 A: CSS text-autospace (新标准,浏览器支持有限) */
  text-autospace: ideograph-alpha ideograph-numeric;

  /* 方案 B: 手动处理 */
  /* 使用 JavaScript 在中文与英文之间插入 thin space */
}
// JavaScript 自动插入中英文间距
function addCJKSpacing(text) {
  return text
    // 中文后跟英文/数字
    .replace(/([\u4e00-\u9fff])([\w])/g, '$1\u2006$2')
    // 英文/数字后跟中文
    .replace(/([\w])([\u4e00-\u9fff])/g, '$1\u2006$2');
  // \u2006 = Six-Per-Em Space (1/6 em)
}

4. 竖排文字

4.1 CSS 竖排

/* 竖排排版 */
.vertical-text {
  writing-mode: vertical-rl;    /* 从右到左竖排 */
  text-orientation: mixed;      /* 中文竖排,英文横转 */

  /* 或 */
  writing-mode: vertical-lr;    /* 从左到右竖排(蒙古文等) */
}

/* 竖排中的数字处理 */
.vertical-text .number {
  text-combine-upright: all;    /* 两位数字横排 */
  /* 或 */
  text-orientation: upright;    /* 数字也竖排 */
}

4.2 竖排常见问题

问题 原因 解决方案
标点位置错误 标点未随文字旋转 font-feature-settings: "vrt2"
英文方向错误 默认旋转90度 text-orientation: mixed
行距不均 全角半角混排 统一使用全角标点
连续数字 3-4位数字挤在一竖 text-combine-upright: digits 2
表情符号 可能被旋转 unicode-bidi: plaintext

5. 国际化(i18n)设计

5.1 多语言界面挑战

挑战 中文 英文 阿拉伯文 日文
文字方向 LTR LTR RTL LTR/竖排
文字长度 短(表意文字) 短-中
换行规则 按字换行 按词换行 按词换行 复合规则
字体体积 大(数万字) 小(百字母)
复数形式 2 种 6 种
数字格式 1,234.56 1,234.56 1,234.56 1,234.56
日期格式 2026年2月28日 Feb 28, 2026 28/02/2026 2026/02/28

5.2 文本扩展预算

翻译后文本长度变化参考(以英文为基准):

目标语言 扩展比例 示例
中文 0.6-0.8x "Save" -> "保存"
日文 0.8-1.2x "Save" -> "保存する"
德文 1.3-1.8x "Save" -> "Speichern"
法文 1.2-1.6x "Save" -> "Sauvegarder"
阿拉伯文 1.2-1.5x 从右到左
俄文 1.3-1.7x "Save" -> "Сохранить"

设计中必须为文本预留 50% 的扩展空间。

5.3 Flex 布局应对文本扩展

/* 不要固定宽度 */
.button {
  /* 错误: width: 80px; */
  /* 正确: */
  min-width: 80px;
  padding: 8px 16px;
  white-space: nowrap;
}

/* 使用 flex 自适应 */
.nav-bar {
  display: flex;
  flex-wrap: wrap;  /* 允许换行,避免溢出 */
  gap: 8px;
}

.nav-item {
  flex-shrink: 0;   /* 不压缩 */
}

5.4 RTL(从右到左)支持

/* 逻辑属性替代物理属性 */
.card {
  /* 物理属性 (不推荐): */
  /* margin-left: 16px; */
  /* padding-right: 24px; */
  /* text-align: left; */

  /* 逻辑属性 (推荐): */
  margin-inline-start: 16px;
  padding-inline-end: 24px;
  text-align: start;
}

/* 图标翻转 */
[dir="rtl"] .icon-arrow-right {
  transform: scaleX(-1);  /* 水平翻转箭头 */
}

/* 但是! 某些图标不应翻转 */
[dir="rtl"] .icon-clock,
[dir="rtl"] .icon-phone,
[dir="rtl"] .icon-search {
  transform: none;  /* 时钟、电话、搜索不翻转 */
}

6. 数字与日期格式化

6.1 Intl API

// 数字格式化
function formatNumber(value, locale) {
  return new Intl.NumberFormat(locale).format(value);
}

formatNumber(1234567.89, 'zh-CN');  // "1,234,567.89"
formatNumber(1234567.89, 'de-DE');  // "1.234.567,89"
formatNumber(1234567.89, 'ja-JP');  // "1,234,567.89"

// 货币格式化
function formatCurrency(value, locale, currency) {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency
  }).format(value);
}

formatCurrency(1234.5, 'zh-CN', 'CNY');  // "CN\u00a51,234.50"
formatCurrency(1234.5, 'en-US', 'USD');  // "$1,234.50"
formatCurrency(1234.5, 'ja-JP', 'JPY');  // "JP\u00a51,235"

// 日期格式化
function formatDate(date, locale) {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
}

formatDate(new Date(), 'zh-CN');  // "2026年2月28日"
formatDate(new Date(), 'en-US');  // "February 28, 2026"
formatDate(new Date(), 'ja-JP');  // "2026年2月28日"

// 相对时间
function formatRelative(date, locale) {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
  const diff = Math.round((date - Date.now()) / (1000 * 60 * 60 * 24));
  return rtf.format(diff, 'day');
}

formatRelative(yesterday, 'zh-CN');  // "昨天"
formatRelative(yesterday, 'en-US');  // "yesterday"

7. 排版质量检查

7.1 自动化检查清单

检查项 规则 工具
字体回退 中文字体不可用时有合理回退 Playwright 截图对比
行首标点 无避头标点出现在行首 正则扫描 + 视觉检查
中英间距 中英文之间有适当间距 自动检测脚本
行长 中文每行 25-40 字 CSS 检查 max-width
行高 中文正文行高 >= 1.6 CSS 检查 line-height
字号 正文 >= 14px CSS 检查 font-size
文本扩展 界面元素容纳 1.5x 文本 伪翻译测试
RTL 布局 逻辑属性替代物理属性 stylelint 规则

7.2 伪翻译测试

// 伪翻译: 用扩展字符替换,测试布局弹性
function pseudoTranslate(text, expansionFactor = 1.5) {
  const expanded = text
    .split('')
    .map(char => {
      if (/[a-zA-Z]/.test(char)) {
        // 用带音调的字符替换,增加视觉标识
        const map = { a: 'a', e: 'e', i: 'i', o: 'o', u: 'u' };
        return (map[char.toLowerCase()] || char).repeat(
          Math.ceil(expansionFactor)
        );
      }
      return char;
    })
    .join('');

  return `[${expanded}]`;  // 方括号标识伪翻译
}

// "Save" -> "[Saavee]"
// "Cancel" -> "[Caannceell]"

8. CJK 特殊场景处理

8.1 表格中的中文

.zh-table {
  /* 中文表格不要 nowrap,允许单元格内换行 */
  word-break: break-all;  /* CJK 允许任意位置换行 */

  /* 数字列右对齐 */
  td.number { text-align: right; font-variant-numeric: tabular-nums; }

  /* 中文列左对齐 */
  td.text { text-align: start; }
}

8.2 输入法兼容

// 中文输入法组合事件处理
let isComposing = false;

input.addEventListener('compositionstart', () => {
  isComposing = true;
});

input.addEventListener('compositionend', (e) => {
  isComposing = false;
  // 在这里处理最终输入,而不是在 input 事件中
  handleInput(e.target.value);
});

input.addEventListener('input', (e) => {
  if (isComposing) return;  // 组合输入中,不触发搜索/验证
  handleInput(e.target.value);
});

8.3 文本截断

/* 中文单行截断 */
.zh-truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 中文多行截断 */
.zh-line-clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;  /* 最多 3 行 */
  overflow: hidden;
}

9. 性能优化

9.1 字体加载策略

策略对比:
+------------------+--------+----------+
| 策略              | FOIT   | FOUT     |
+------------------+--------+----------+
| font-display:auto | 3s闪白 | 无       |
| font-display:swap | 无     | 字体闪跳 |
| font-display:optional | 无 | 无(可能不加载)|
| 预加载 + swap    | 极短   | 极短     |
+------------------+--------+----------+

推荐方案: 预加载 + swap
<!-- 预加载中文字体 -->
<link
  rel="preload"
  href="/fonts/SourceHanSansSC-Regular-subset.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

9.2 按需加载

// Google Fonts 按 unicode-range 分片加载
// 浏览器只下载页面中实际使用到的字符范围

@font-face {
  font-family: 'Noto Sans SC';
  src: url('NotoSansSC-Regular.1.woff2') format('woff2');
  unicode-range: U+fee3, U+fef3, U+ff03-ff04, U+ff07, U+ff0a-ff0b;
}

@font-face {
  font-family: 'Noto Sans SC';
  src: url('NotoSansSC-Regular.2.woff2') format('woff2');
  unicode-range: U+5408, U+5409, U+540a-540d, U+540f-5411, U+5413;
}
/* ... 数十个分片 */

10. 工具与参考

工具 用途
pyftsubset (fonttools) 字体子集化
cn-font-split 中文字体自动分包
Google Fonts 中文 Web 字体 CDN
text-autospace polyfill 中英间距自动处理
stylelint-i18n i18n CSS 规则检查
ICU MessageFormat 国际化消息格式
Intl API 浏览器原生国际化
i18next 前端 i18n 框架

参考规范:

规范 说明
W3C CLReq 中文排版需求 (Requirements for Chinese Text Layout)
W3C JLReq 日文排版需求
Unicode UAX #14 Unicode 换行算法
Unicode UAX #11 东亚字符宽度
CSS Writing Modes L4 竖排排版规范
CSS Text L4 文本处理规范

Maurice | maurice_wen@proton.me