Font related improvements

This commit is contained in:
2026-08-13 12:05:47 -07:00
parent 4eda93cf24
commit acc4c117f7
19 changed files with 317 additions and 41 deletions
+7
View File
@@ -56,6 +56,13 @@ build 把同一个 build ID 注入浏览器 bundle 与 worker,发现更新后
metadata 的唯一文案来源。Vite 在构建时解析并注入它,Rust 则嵌入同一文件来验证允许保存的 locale。增加文案键时必须为每个 locale 提供值;增加语言时同时增加完整
`[locales."<code>".messages]` 段。业务命令如“点歌”是 Bilibili 输入协议,不属于 UI 翻译。
## 字体资源
Noto Serif SC、ZCOOL XiaoWei 与 LXGW
WenKai 由锁定的 Fontsource 依赖提供;漓雨手书则以固定版本的 WOFF2 文件保存在
`public/fonts/`。浏览器从 Rust 静态服务同域加载这些字体,不依赖 OBS 设备的系统字体。每套字体的 OFL-1.1 许可证均输出到
`/fonts/licenses/`。
## 格式化与构建
```bash
+30
View File
@@ -8,6 +8,9 @@
"name": "lxc-stream-overlay",
"version": "0.1.0",
"dependencies": {
"@fontsource/lxgw-wenkai": "5.3.0",
"@fontsource/noto-serif-sc": "5.3.0",
"@fontsource/zcool-xiaowei": "5.3.0",
"react": "19.1.1",
"react-dom": "19.1.1"
},
@@ -745,6 +748,33 @@
"node": ">=18"
}
},
"node_modules/@fontsource/lxgw-wenkai": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/lxgw-wenkai/-/lxgw-wenkai-5.3.0.tgz",
"integrity": "sha512-+U5AAOXeaB7o+3G0Qm7I7Djz2Jd1hqNY+kClOZH8bTunyyGa2WxY12LZftt2AqUsxlfxwdQpd75Js1HQIt4H4g==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/noto-serif-sc": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/noto-serif-sc/-/noto-serif-sc-5.3.0.tgz",
"integrity": "sha512-0/zaEFkidiWldE62rTeD74x8ygUsQvejiSNtO0LQxQk3qpaHnlMZ3w4C7yH80B4KTIg8VKeFP4oSgwWMchY9+g==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/zcool-xiaowei": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/zcool-xiaowei/-/zcool-xiaowei-5.3.0.tgz",
"integrity": "sha512-iRuUPl9ONFbYzi6JcEshpE4VedINyzQ4hv0Z5XEt0cB0pMPiQeqc2Qvv1NwJCNzkPwG02VCdcO+cF2K8ylYn2A==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+4 -1
View File
@@ -3,13 +3,16 @@
"private": true,
"version": "0.1.0",
"scripts": {
"build": "tsc -b && vite build",
"build": "tsc -b && vite build && node scripts/check-font-assets.mjs",
"dev": "vite",
"docs:check": "node scripts/check-doc-links.mjs ../..",
"format": "prettier --ignore-path ../../.prettierignore --write . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml",
"format:check": "prettier --ignore-path ../../.prettierignore --check . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml"
},
"dependencies": {
"@fontsource/lxgw-wenkai": "5.3.0",
"@fontsource/noto-serif-sc": "5.3.0",
"@fontsource/zcool-xiaowei": "5.3.0",
"react": "19.1.1",
"react-dom": "19.1.1"
},
+19
View File
@@ -0,0 +1,19 @@
# Bundled font notice
## Liyu Shoushu
`liyu-shoushu-v0.107.woff2` is a WOFF2 conversion of `LiyuShoushu.ttf` from the official
[Liyu Shoushu v0.107 release](https://github.com/InkPantherType/LiyuShoushu/releases/tag/v0.107).
- Release tag commit: `219a0f42cb7b3f0d00be6e5bf9f8b5298a1f3060`
- Original TTF SHA-256: `16554439b7e95da1fc354ed7b2c1551e83e942c3c5afb2a2e883761c6037dc22`
- Bundled WOFF2 SHA-256: `b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3`
- Conversion: FontTools 4.63.0 WOFF2 compression with Brotli 1.2.0; no glyph subsetting
- Coverage retained: 18,828 glyphs and 18,221 Unicode mappings
- License: SIL Open Font License 1.1; see
[`licenses/liyu-shoushu-OFL.txt`](licenses/liyu-shoushu-OFL.txt)
The original font credits Yuji Kataoka and the Yuji Project Authors for the base brush glyphs, LXGW
and the LXGW ZhenKai Project Authors for punctuation and symbols, and Yuchen Tian's
[zi2zi-JiT](https://github.com/kaonashi-tyc/zi2zi-JiT) for generated Chinese glyphs. This notice
preserves the zi2zi-JiT attribution required by the upstream font documentation.
@@ -0,0 +1,94 @@
Copyright 2026 Alexander Tseng.
Portions Copyright 2021 The Yuji Project Authors (https://github.com/Kinutafontfactory/Yuji).
Portions Copyright LXGW and LXGWZhenKai Project Authors (https://github.com/lxgw/LxgwZhenKai).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+1
View File
@@ -66,6 +66,7 @@ function isControlShell(pathname) {
function isStaticAsset(pathname) {
return (
pathname.startsWith('/assets/') ||
pathname.startsWith('/fonts/') ||
pathname.startsWith('/pwa/') ||
pathname === '/control/manifest.webmanifest'
)
@@ -0,0 +1,67 @@
import { createHash } from 'node:crypto'
import { readdir, readFile } from 'node:fs/promises'
const dist = new URL('../dist/', import.meta.url)
async function filesBelow(directory, prefix = '') {
const entries = await readdir(directory, { withFileTypes: true })
const files = []
for (const entry of entries) {
const relative = `${prefix}${entry.name}`
if (entry.isDirectory()) {
files.push(...(await filesBelow(new URL(`${entry.name}/`, directory), `${relative}/`)))
} else {
files.push(relative)
}
}
return files
}
const files = await filesBelow(dist)
const expectedFonts = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai']
const liyuFont = 'fonts/liyu-shoushu-v0.107.woff2'
for (const font of expectedFonts) {
if (!files.some(file => file.startsWith(`assets/${font}-`) && file.endsWith('.woff2'))) {
throw new Error(`Missing bundled WOFF2 assets for ${font}`)
}
if (!files.includes(`fonts/licenses/${font}-OFL.txt`)) {
throw new Error(`Missing bundled license for ${font}`)
}
}
if (!files.includes(liyuFont)) throw new Error('Missing bundled WOFF2 asset for liyu-shoushu')
if (!files.includes('fonts/licenses/liyu-shoushu-OFL.txt')) {
throw new Error('Missing bundled license for liyu-shoushu')
}
const liyuDigest = createHash('sha256')
.update(await readFile(new URL(liyuFont, dist)))
.digest('hex')
if (liyuDigest !== 'b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3') {
throw new Error(`Unexpected liyu-shoushu WOFF2 digest: ${liyuDigest}`)
}
const legacyWoff = files.find(file => file.endsWith('.woff'))
if (legacyWoff) throw new Error(`Unexpected legacy WOFF asset: ${legacyWoff}`)
const css = (
await Promise.all(
files.filter(file => file.endsWith('.css')).map(file => readFile(new URL(file, dist), 'utf8')),
)
).join('\n')
for (const family of ['Noto Serif SC', 'ZCOOL XiaoWei', 'LXGW WenKai', 'Liyu Shoushu']) {
const declarations = [
`font-family:${family}`,
`font-family:${JSON.stringify(family)}`,
`font-family:'${family}'`,
]
if (!declarations.some(declaration => css.includes(declaration))) {
throw new Error(`Missing @font-face declaration for ${family}`)
}
}
if (!css.includes('font-display:block') || css.includes('font-display:swap')) {
throw new Error('Bundled fonts must block local fallback while loading')
}
console.log('Verified same-origin WOFF2 component fonts and licenses.')
+7
View File
@@ -0,0 +1,7 @@
@font-face {
font-family: 'Liyu Shoushu';
font-style: normal;
font-display: block;
font-weight: 400;
src: url('/fonts/liyu-shoushu-v0.107.woff2') format('woff2');
}
+4
View File
@@ -25,6 +25,10 @@ import { SongRequestOverlay } from './songOverlay'
import { useComponentStream } from './stream'
import { I18nProvider, translate, useI18n } from './i18n'
import type { Session } from './types'
import '@fontsource/noto-serif-sc/400.css'
import '@fontsource/zcool-xiaowei/400.css'
import '@fontsource/lxgw-wenkai/500.css'
import './fonts.css'
import './style.css'
import './control.css'
import './giftEffect.css'
+2
View File
@@ -105,6 +105,8 @@
}
.gift-menu-overlay.gift-menu-theme-moonlit-water {
height: 100%;
align-items: center;
padding-block: calc(clamp(8px, 1.3vmin, 16px) + var(--moonlit-edge-space));
}
+7 -5
View File
@@ -1,7 +1,7 @@
import type { CSSProperties } from 'react'
/** Stable IDs persisted by the backend; each maps to a fixed, injection-safe stack. */
export const fontFamilyIds = ['song', 'fang-song', 'kai'] as const
/** Stable IDs persisted by the backend; each maps to a bundled, same-origin webfont. */
export const fontFamilyIds = ['song', 'fang-song', 'kai', 'liyu-shoushu'] as const
export type FontFamilyId = (typeof fontFamilyIds)[number]
@@ -9,12 +9,14 @@ export const fontFamilies: ReadonlyArray<{ id: FontFamilyId; nameKey: string }>
{ id: 'song', nameKey: 'settings.font_family.song' },
{ id: 'fang-song', nameKey: 'settings.font_family.fang_song' },
{ id: 'kai', nameKey: 'settings.font_family.kai' },
{ id: 'liyu-shoushu', nameKey: 'settings.font_family.liyu_shoushu' },
]
const fontStacks: Record<FontFamilyId, string> = {
song: '"Source Han Serif SC", "Noto Serif CJK SC", "Noto Serif SC", "Songti SC", STSong, SimSun, serif',
'fang-song': 'FangSong, STFangsong, "FZShuSong-Z01", "Noto Serif SC", "Songti SC", serif',
kai: '"Kaiti SC", STKaiti, KaiTi, "LXGW WenKai", "Noto Serif SC", serif',
song: '"Noto Serif SC", serif',
'fang-song': '"ZCOOL XiaoWei", serif',
kai: '"LXGW WenKai", serif',
'liyu-shoushu': '"Liyu Shoushu", serif',
}
export function normalizeFontFamilyId(value: unknown): FontFamilyId {
+20
View File
@@ -14,6 +14,13 @@ import { parse } from 'smol-toml'
// The waiting worker is still activated explicitly by the user in the console.
const pwaBuildId = Date.now().toString(36)
const serviceWorkerSource = readFileSync(new URL('./pwa/control-sw.js', import.meta.url), 'utf8')
const bundledFontLicenses = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai'].map(font => ({
fileName: `fonts/licenses/${font}-OFL.txt`,
source: readFileSync(
new URL(`./node_modules/@fontsource/${font}/LICENSE`, import.meta.url),
'utf8',
),
}))
const i18nResource = parse(
readFileSync(new URL('../../resources/i18n.toml', import.meta.url), 'utf8'),
) as unknown as {
@@ -51,6 +58,16 @@ const manifest = {
export default defineConfig({
plugins: [
react(),
{
name: 'same-origin-woff2-fonts',
enforce: 'pre',
transform(source, id) {
if (!id.includes('/node_modules/@fontsource/') || !id.endsWith('.css')) return
return source
.replaceAll('font-display: swap', 'font-display: block')
.replace(/,\s*url\([^)]*\.woff\) format\(['"]woff['"]\)/g, '')
},
},
{
name: 'control-pwa-assets',
transformIndexHtml(html) {
@@ -61,6 +78,9 @@ export default defineConfig({
.replaceAll('__APP_DESCRIPTION__', defaultMessages['pwa.manifest.description'])
},
generateBundle() {
for (const license of bundledFontLicenses) {
this.emitFile({ type: 'asset', ...license })
}
this.emitFile({
type: 'asset',
fileName: 'control/sw.js',
+1
View File
@@ -956,6 +956,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libilibili"
version = "0.1.0"
source = "git+http://gitea.home.arpa/felis/libilibili.git#3cb2a902d4d9387c5581a1827cc19248b6000ae2"
dependencies = [
"base64",
"brotli",
+14 -4
View File
@@ -7,20 +7,30 @@ edition = "2024"
axum = { version = "0.8", features = ["ws", "json"] }
async-trait = "0.1"
base64 = "0.22"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
chrono = { version = "0.4", default-features = false, features = [
"clock",
"serde",
] }
chacha20poly1305 = "0.10"
deadpool-postgres = "0.14"
# Kept as a sibling checkout during development. Docker Compose supplies the
# same directory as a named BuildKit context at `/libilibili`.
libilibili = { path = "../../../libilibili" }
libilibili = { git = "http://gitea.home.arpa/felis/libilibili.git" }
rand = "0.9"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
subtle = "2"
tokio = { version = "1", features = ["full"] }
tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1", "with-chrono-0_4"] }
tokio-postgres = { version = "0.7", features = [
"with-serde_json-1",
"with-uuid-1",
"with-chrono-0_4",
] }
tokio-util = "0.7"
toml = "0.8"
totp-rs = { version = "5.7", features = ["gen_secret", "qr", "zeroize"] }
+8 -3
View File
@@ -1,8 +1,8 @@
//! Shared, validated typography settings for all built-in OBS components.
//!
//! Font IDs map to fixed frontend font stacks rather than accepting arbitrary
//! CSS. This keeps settings portable between OBS hosts and prevents untrusted
//! component settings from becoming a CSS injection boundary.
//! Font IDs map to fixed, same-origin WOFF2 assets in the frontend rather than
//! accepting arbitrary CSS. This keeps settings portable between OBS hosts and
//! prevents untrusted component settings from becoming a CSS injection boundary.
use serde::{Deserialize, Serialize};
@@ -13,6 +13,7 @@ pub enum FontFamilyId {
#[default]
FangSong,
Kai,
LiyuShoushu,
}
pub const fn default_font_brightness() -> u16 {
@@ -35,5 +36,9 @@ mod tests {
);
assert_eq!(sanitize_font_brightness(1), 70);
assert_eq!(sanitize_font_brightness(u16::MAX), 180);
assert_eq!(
serde_json::to_string(&FontFamilyId::LiyuShoushu).unwrap(),
"\"liyu-shoushu\""
);
}
}
+22 -20
View File
@@ -12,28 +12,30 @@
## 设置
| 字段 | 范围/单位 | 行为 |
| ------------------------ | ----------- | -------------------------- |
| `themeId` | 主题 ID | 选择已安装的完整视觉主题 |
| `fontFamily` | 字体 ID | `song`、`fang-song`、`kai` |
| `fontBrightness` | 70–180% | 只调整文字亮度 |
| `viewerColor` | `#RRGGBB` | 可选的用户昵称颜色覆盖 |
| `danmakuColor` | `#RRGGBB` | 可选的弹幕正文颜色覆盖 |
| `fontScale` | 50–300% | 展开与收缩字号的统一比例 |
| `decorationLineWeight` | 50–300% | 边框、分隔线与古风边缘粗细 |
| `maxVisible` | 1–12 | 同时保留的消息卡数量 |
| `collapseAfterSeconds` | 2–120 秒 | 最新卡从展开态切换到紧凑态 |
| `unfoldDurationMs` | 200–5000 ms | 横向卷轴展开动画时间 |
| `motionIntensity` | 0–100% | 卡片、流光与焦点动画强度 |
| `particleCount` | 0–12 | 每卡星花粒子数量 |
| `particleSpeed` | 25–300% | 粒子动画速度 |
| `lowPerformanceMode` | boolean | 关闭高成本动态效果 |
| `highValueThreshold` | 千分之一元 | 高价值礼物起点 |
| `featuredValueThreshold` | 千分之一元 | 焦点礼物起点 |
| `show*` | boolean | 控制各事件类别订阅 |
| 字段 | 范围/单位 | 行为 |
| ------------------------ | ----------- | ------------------------------------------ |
| `themeId` | 主题 ID | 选择已安装的完整视觉主题 |
| `fontFamily` | 字体 ID | `song`、`fang-song`、`kai`、`liyu-shoushu` |
| `fontBrightness` | 70–180% | 只调整文字亮度 |
| `viewerColor` | `#RRGGBB` | 可选的用户昵称颜色覆盖 |
| `danmakuColor` | `#RRGGBB` | 可选的弹幕正文颜色覆盖 |
| `fontScale` | 50–300% | 展开与收缩字号的统一比例 |
| `decorationLineWeight` | 50–300% | 边框、分隔线与古风边缘粗细 |
| `maxVisible` | 1–12 | 同时保留的消息卡数量 |
| `collapseAfterSeconds` | 2–120 秒 | 最新卡从展开态切换到紧凑态 |
| `unfoldDurationMs` | 200–5000 ms | 横向卷轴展开动画时间 |
| `motionIntensity` | 0–100% | 卡片、流光与焦点动画强度 |
| `particleCount` | 0–12 | 每卡星花粒子数量 |
| `particleSpeed` | 25–300% | 粒子动画速度 |
| `lowPerformanceMode` | boolean | 关闭高成本动态效果 |
| `highValueThreshold` | 千分之一元 | 高价值礼物起点 |
| `featuredValueThreshold` | 千分之一元 | 焦点礼物起点 |
| `show*` | boolean | 控制各事件类别订阅 |
后端的 `OverlaySettings::sanitize` 是最终边界。控制台 range
input 只改善交互,不能取代服务端校验。字体 ID 只映射到前端预先注册的安全字体栈,不接受任意 CSS;OBS 设备缺少首选字体时会依次回退到可用衬线字体。颜色覆盖同样只接受六位十六进制颜色;留空时使用主题色,切换主题后会自动采用新主题的配色。
input 只改善交互,不能取代服务端校验。字体 ID 只映射到前端预先注册并由服务同域提供的 WOFF2 字体,不接受任意 CSS,也不依赖 OBS 设备安装的字体;`song`、`fang-song`、`kai`
与 `liyu-shoushu` 分别使用 Noto Serif SC、ZCOOL XiaoWei、LXGW
WenKai 与漓雨手书。颜色覆盖同样只接受六位十六进制颜色;留空时使用主题色,切换主题后会自动采用新主题的配色。
## 主题
View File
+10 -8
View File
@@ -142,10 +142,11 @@ name = "简体中文"
"nav.logout" = "退出"
"settings.theme" = "主题"
"settings.font_family" = "中文字体"
"settings.font_family_description" = "按 OBS 所在设备已安装字体依次匹配;缺失时自动回退到衬线字体。"
"settings.font_family.song" = "宋体 · 端正"
"settings.font_family.fang_song" = "仿宋 · 清雅"
"settings.font_family.kai" = "楷体 · 书卷"
"settings.font_family_description" = "字体随服务提供并由 OBS 同域加载,不依赖设备已安装字体。"
"settings.font_family.song" = "思源宋体 · 端正"
"settings.font_family.fang_song" = "小薇体 · 清雅"
"settings.font_family.kai" = "霞鹜文楷 · 书卷"
"settings.font_family.liyu_shoushu" = "漓雨手书 · 毛笔手写"
"settings.font_brightness" = "文字亮度"
"settings.font_brightness_description" = "只提亮文字,不改变透明背景、礼物图片或直播画面。"
"settings.viewer_color" = "用户昵称颜色"
@@ -642,10 +643,11 @@ name = "English"
"nav.logout" = "Log out"
"settings.theme" = "Theme"
"settings.font_family" = "Chinese typeface"
"settings.font_family_description" = "Uses the first installed font on the OBS device and falls back safely to a serif face."
"settings.font_family.song" = "Song · Formal"
"settings.font_family.fang_song" = "FangSong · Refined"
"settings.font_family.kai" = "Kai · Calligraphic"
"settings.font_family_description" = "Fonts are served by this service and loaded from the same origin instead of relying on fonts installed on the OBS device."
"settings.font_family.song" = "Noto Serif SC · Formal"
"settings.font_family.fang_song" = "ZCOOL XiaoWei · Refined"
"settings.font_family.kai" = "LXGW WenKai · Calligraphic"
"settings.font_family.liyu_shoushu" = "Liyu Shoushu · Brush handwriting"
"settings.font_brightness" = "Text brightness"
"settings.font_brightness_description" = "Brightens text only; transparency, gift artwork, and the underlying scene are unchanged."
"settings.viewer_color" = "Viewer name color"