feat(settings): add curated multilingual fonts

Bundle Vazirmatn, Noto Sans Hebrew, Noto Sans SC, and Roboto with locale-aware fallbacks while keeping custom font import out of scope.

Refs #31
This commit is contained in:
NimBold
2026-07-23 17:29:29 +03:30
parent 1f1ec828f9
commit b579c35e42
17 changed files with 157 additions and 7 deletions
+15
View File
@@ -4,6 +4,21 @@ Firelink distributes separate executable tools. Firelink's MIT license does not
Exact versions, target hashes, sources, and build descriptions are pinned in `engines.lock.json`.
## Bundled fonts
Firelink bundles variable WOFF2 web fonts through Fontsource. Each font is
licensed under the SIL Open Font License, Version 1.1. The corresponding
license and author information are available from the linked project sources.
- Inter: <https://github.com/rsms/inter>
- Outfit: <https://github.com/Outfitio/Outfit-Fonts>
- Roboto: <https://github.com/googlefonts/roboto-classic>
- Vazirmatn: <https://github.com/rastikerdar/vazirmatn>
- Noto Sans Hebrew: <https://github.com/notofonts/hebrew>
- Noto Sans SC: <https://github.com/notofonts/noto-cjk>
Fontsource distribution: <https://github.com/fontsource/font-files>
## aria2
- Project: <https://aria2.github.io/>
+40
View File
@@ -10,7 +10,11 @@
"license": "MIT",
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/noto-sans-hebrew": "^5.3.0",
"@fontsource-variable/noto-sans-sc": "^5.3.0",
"@fontsource-variable/outfit": "^5.3.0",
"@fontsource-variable/roboto": "^5.3.0",
"@fontsource-variable/vazirmatn": "^5.3.0",
"@formkit/auto-animate": "^0.10.0",
"@tailwindcss/vite": "^4.3.3",
"@tauri-apps/api": "^2.11.1",
@@ -92,6 +96,24 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/noto-sans-hebrew": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-hebrew/-/noto-sans-hebrew-5.3.0.tgz",
"integrity": "sha512-jGIZ2EOCTtT1ZhLthz6nEpPXGMniSPI/0tv5CDFDZ7MPYyoj6FskW7WJ0YTyesFADdzjx7F2Pg5W1PWD6y9UFw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/noto-sans-sc": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.3.0.tgz",
"integrity": "sha512-lNar1dF7Ik/lHNPo/7JWG0TolXY29LtsqYgMvEysooZ5bsO9uH4shJmRrwyJ3PjyTPljhpMJEK0jDuLSU4vJ1w==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/outfit": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/outfit/-/outfit-5.3.0.tgz",
@@ -101,6 +123,24 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/roboto": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/roboto/-/roboto-5.3.0.tgz",
"integrity": "sha512-BoaaZiQf8fgtxv07KXkTwx1bKz/EoqIlTXq2OOKKXINxp6qXWdnlChqV+hSaDANS49yyUASFl3OVBMMDTCIHFA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/vazirmatn": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/vazirmatn/-/vazirmatn-5.3.0.tgz",
"integrity": "sha512-ZA68W8JHbMdNzzwjYHUWKB2PH22QpxNEnk0ukcXM+XiqYMV6ZWJZAKuNXcrMR2JjuIAwsfAU3KknCXPI6wXUvw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@formkit/auto-animate": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.10.0.tgz",
+4
View File
@@ -40,7 +40,11 @@
},
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/noto-sans-hebrew": "^5.3.0",
"@fontsource-variable/noto-sans-sc": "^5.3.0",
"@fontsource-variable/outfit": "^5.3.0",
"@fontsource-variable/roboto": "^5.3.0",
"@fontsource-variable/vazirmatn": "^5.3.0",
"@formkit/auto-animate": "^0.10.0",
"@tailwindcss/vite": "^4.3.3",
"@tauri-apps/api": "^2.11.1",
+5 -1
View File
@@ -215,12 +215,16 @@ pub enum Theme {
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[serde(rename_all = "kebab-case")]
#[ts(export, export_to = "../../src/bindings/")]
pub enum FontFamily {
System,
Inter,
Outfit,
Vazirmatn,
NotoSansHebrew,
NotoSansSc,
Roboto,
Serif,
Monospace,
}
+25 -1
View File
@@ -196,7 +196,17 @@ fn sanitize_persisted_setting_values(state: &mut Value) {
sanitize_allowed_string(
state,
"fontFamily",
&["system", "inter", "outfit", "serif", "monospace"],
&[
"system",
"inter",
"outfit",
"vazirmatn",
"noto-sans-hebrew",
"noto-sans-sc",
"roboto",
"serif",
"monospace",
],
);
sanitize_allowed_string(
state,
@@ -704,6 +714,20 @@ mod tests {
assert!(matches!(settings.font_family, FontFamily::System));
}
#[test]
fn curated_font_families_are_preserved() {
for value in ["vazirmatn", "noto-sans-hebrew", "noto-sans-sc", "roboto"] {
let stored = json!({
"state": {"fontFamily": value},
"version": 5
});
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
assert_eq!(serde_json::to_value(settings.font_family).unwrap(), json!(value));
}
}
#[test]
fn invalid_window_control_style_uses_automatic_layout() {
let stored = json!({
+1 -1
View File
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FontFamily = "system" | "inter" | "outfit" | "serif" | "monospace";
export type FontFamily = "system" | "inter" | "outfit" | "vazirmatn" | "noto-sans-hebrew" | "noto-sans-sc" | "roboto" | "serif" | "monospace";
+4
View File
@@ -711,6 +711,10 @@ runEngineChecks(false);
{ value: 'system', label: t($ => $.settings.lookAndFeel.fontFamilySystem) },
{ value: 'inter', label: t($ => $.settings.lookAndFeel.fontFamilyInter) },
{ value: 'outfit', label: t($ => $.settings.lookAndFeel.fontFamilyOutfit) },
{ value: 'vazirmatn', label: t($ => $.settings.lookAndFeel.fontFamilyVazirmatn) },
{ value: 'noto-sans-hebrew', label: t($ => $.settings.lookAndFeel.fontFamilyNotoSansHebrew) },
{ value: 'noto-sans-sc', label: t($ => $.settings.lookAndFeel.fontFamilyNotoSansSc) },
{ value: 'roboto', label: t($ => $.settings.lookAndFeel.fontFamilyRoboto) },
{ value: 'serif', label: t($ => $.settings.lookAndFeel.fontFamilySerif) },
{ value: 'monospace', label: t($ => $.settings.lookAndFeel.fontFamilyMonospace) },
];
+4
View File
@@ -623,6 +623,10 @@ const common = {
fontFamilySystem: 'System UI (recommended)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilyVazirmatn: 'Vazirmatn (Persian)',
fontFamilyNotoSansHebrew: 'Noto Sans Hebrew',
fontFamilyNotoSansSc: 'Noto Sans SC (Simplified Chinese)',
fontFamilyRoboto: 'Roboto',
fontFamilySerif: 'Serif',
fontFamilyMonospace: 'Monospace',
fontSize: 'Font size',
+4
View File
@@ -623,6 +623,10 @@ const fa = {
fontFamilySystem: 'رابط کاربری سیستم (پیشنهادشده)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilyVazirmatn: 'Vazirmatn (فارسی)',
fontFamilyNotoSansHebrew: 'Noto Sans Hebrew (عبری)',
fontFamilyNotoSansSc: 'Noto Sans SC (چینی ساده‌شده)',
fontFamilyRoboto: 'Roboto',
fontFamilySerif: 'سریف',
fontFamilyMonospace: 'تک‌فاصله',
fontSize: 'اندازه قلم',
+4
View File
@@ -623,6 +623,10 @@ const he = {
fontFamilySystem: 'ממשק המערכת (מומלץ)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilyVazirmatn: 'Vazirmatn (פרסית)',
fontFamilyNotoSansHebrew: 'Noto Sans Hebrew (עברית)',
fontFamilyNotoSansSc: 'Noto Sans SC (סינית מפושטת)',
fontFamilyRoboto: 'Roboto',
fontFamilySerif: 'סריף',
fontFamilyMonospace: 'רוחב קבוע',
fontSize: 'גודל גופן',
+4
View File
@@ -623,6 +623,10 @@ const ru = {
fontFamilySystem: 'Системный интерфейс (рекомендуется)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilyVazirmatn: 'Vazirmatn (персидский)',
fontFamilyNotoSansHebrew: 'Noto Sans Hebrew (иврит)',
fontFamilyNotoSansSc: 'Noto Sans SC (упрощённый китайский)',
fontFamilyRoboto: 'Roboto',
fontFamilySerif: 'С засечками',
fontFamilyMonospace: 'Моноширинный',
fontSize: 'Размер шрифта',
+4
View File
@@ -623,6 +623,10 @@ const uk = {
fontFamilySystem: 'Системний інтерфейс (рекомендовано)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilyVazirmatn: 'Vazirmatn (перська)',
fontFamilyNotoSansHebrew: 'Noto Sans Hebrew (іврит)',
fontFamilyNotoSansSc: 'Noto Sans SC (спрощена китайська)',
fontFamilyRoboto: 'Roboto',
fontFamilySerif: 'Із зарубками',
fontFamilyMonospace: 'Моноширинний',
fontSize: 'Розмір шрифту',
+4
View File
@@ -623,6 +623,10 @@ const zhCN = {
fontFamilySystem: '系统界面字体(推荐)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilyVazirmatn: 'Vazirmatn(波斯语)',
fontFamilyNotoSansHebrew: 'Noto Sans Hebrew(希伯来语)',
fontFamilyNotoSansSc: 'Noto Sans SC(简体中文)',
fontFamilyRoboto: 'Roboto',
fontFamilySerif: '衬线',
fontFamilyMonospace: '等宽',
fontSize: '字体大小',
+1
View File
@@ -96,6 +96,7 @@ describe('translation catalogs', () => {
'settings.lookAndFeel.nord',
'settings.lookAndFeel.fontFamilyInter',
'settings.lookAndFeel.fontFamilyOutfit',
'settings.lookAndFeel.fontFamilyRoboto',
'settings.lookAndFeel.windowControlStyleWindows',
'settings.lookAndFeel.windowControlStyleGnome',
'settings.network.chromeWindows',
+23 -3
View File
@@ -232,21 +232,25 @@
--font-stack-system: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, var(--font-locale-fallback), "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--font-stack-inter: "Inter Variable", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, var(--font-locale-fallback), "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--font-stack-outfit: "Outfit Variable", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, var(--font-locale-fallback), "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--font-stack-vazirmatn: "Vazirmatn Variable", var(--font-stack-system);
--font-stack-noto-sans-hebrew: "Noto Sans Hebrew Variable", var(--font-stack-system);
--font-stack-noto-sans-sc: "Noto Sans SC Variable", var(--font-stack-system);
--font-stack-roboto: "Roboto Variable", var(--font-stack-system);
--font-stack-serif: Georgia, "Times New Roman", var(--font-locale-fallback), serif;
--font-stack-monospace: ui-monospace, "SF Mono", Monaco, "Cascadia Code", "Fira Code", "JetBrains Mono", var(--font-locale-fallback), monospace;
--font-sans: var(--font-stack-system);
}
html[lang="fa"] {
--font-locale-fallback: "Vazirmatn", "Noto Sans Arabic", "Segoe UI", Tahoma, Arial, sans-serif;
--font-locale-fallback: "Geeza Pro", "Noto Sans Arabic", "Segoe UI", Tahoma, Arial, sans-serif;
}
html[lang="he"] {
--font-locale-fallback: "Arial Hebrew", "Noto Sans Hebrew", "Segoe UI", Arial, sans-serif;
--font-locale-fallback: "Arial Hebrew", "Segoe UI", Arial, sans-serif;
}
html[lang="zh-CN"] {
--font-locale-fallback: "PingFang SC", "Microsoft YaHei UI", "Noto Sans CJK SC", "Noto Sans SC", sans-serif;
--font-locale-fallback: "PingFang SC", "Microsoft YaHei UI", "Noto Sans CJK SC", sans-serif;
}
html[lang="ru"],
@@ -266,6 +270,22 @@ html[data-font-family="outfit"] {
--font-sans: var(--font-stack-outfit);
}
html[data-font-family="vazirmatn"] {
--font-sans: var(--font-stack-vazirmatn);
}
html[data-font-family="noto-sans-hebrew"] {
--font-sans: var(--font-stack-noto-sans-hebrew);
}
html[data-font-family="noto-sans-sc"] {
--font-sans: var(--font-stack-noto-sans-sc);
}
html[data-font-family="roboto"] {
--font-sans: var(--font-stack-roboto);
}
html[data-font-family="serif"] {
--font-sans: var(--font-stack-serif);
}
+4
View File
@@ -1,7 +1,11 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "@fontsource-variable/inter/wght.css";
import "@fontsource-variable/noto-sans-hebrew/wght.css";
import "@fontsource-variable/noto-sans-sc/wght.css";
import "@fontsource-variable/outfit/wght.css";
import "@fontsource-variable/roboto/wght.css";
import "@fontsource-variable/vazirmatn/wght.css";
import "./index.css";
import App from "./App";
import { i18nReady } from "./i18n";
+11 -1
View File
@@ -75,7 +75,17 @@ const notifySettingsPersistenceError = () => {
};
const THEME_VALUES = ['system', 'light', 'dark', 'dracula', 'nord'] as const;
const FONT_FAMILY_VALUES = ['system', 'inter', 'outfit', 'serif', 'monospace'] as const;
const FONT_FAMILY_VALUES = [
'system',
'inter',
'outfit',
'vazirmatn',
'noto-sans-hebrew',
'noto-sans-sc',
'roboto',
'serif',
'monospace',
] as const;
const WINDOW_CONTROL_STYLE_VALUES = ['auto', 'macos', 'windows', 'gnome', 'minimal'] as const;
const APP_FONT_SIZE_VALUES = ['small', 'standard', 'large'] as const;
const LIST_ROW_DENSITY_VALUES = ['compact', 'standard', 'relaxed'] as const;