How the UI speaks English, German and Chinese and how it is coloured — spec 030, done 2026-10-03 on the spec-030 branch.
Languages
- Choose in Settings → Appearance → Language: System (the default, empty
Settings.language), English, Deutsch, 中文 (简体). The change applies live, no restart. - System language:
moonkale_ext_api::i18n::system_language()(sys-locale; itsjsfeature readsnavigator.languageson the web). It is read on the client inload_user_settingsand stored inSettingsState.system_language, which starts as"en"— the server’s SSR and the first client render must agree (hydration), so a German browser sees English for one frame. - Strings: Fluent (
fluent-bundle0.16). Every crate with UI text haslocales/{en,de,zh-CN}.ftland apub(crate) static L: Locales; look up witht!(ws, L, "id")ort!(ws, L, "id", count = n). English is required, a missing id falls back to English, then to the id itself. Ids are keys, not English text. - Extensions bring their own strings through
Extension::locales(); the Extensions panel showsextension-name/extension-descriptionfrom them. - Checks:
tools/check-strings.py(CI lint) parses every.ftl, checks that each id used in Rust exists in English and that German and Chinese have the same ids and the same variables. 618 strings in 15 crates at the time of writing. - Rule (P-151):
Workspace::lang()subscribes to the settings signal. Code that runs inside an untracked command effect wraps its calls inworkspace::untracked(…), wherelang()peeks. - Not yet: German and Chinese are machine-quality drafts awaiting a native review (Daniel for German). The JS bundles’ few own phrases (CodeMirror’s search panel) are still English.
Themes
- Choose in Settings → Appearance → Theme: Dark (default), Light, Follow system, and every theme file found.
- Tokens: every colour is a
--mk-*custom property, defined for Dark and Light inpackages/shell/src/theme.rs(TOKENS, about 94) and nowhere else. dioxus-workbench’s--wb-*variables are mapped onto them under:root .wb-shell, :root .wb-workspace(higher specificity than the workbench’s own palette).tools/check-colors.py(ceiling 0, intools/colors-max.txt) fails CI on any hex/rgb()/hsl()colour outside the theme module (vendoredxterm.css,milkdown.cssexcepted). - How it applies: the built-in sheet is rendered once as
style { dangerous_inner_html }(P-152) with:root[data-theme=dark|light]rules and aprefers-color-schemerule for Follow system; the frame setsdata-themeon<html>by eval and dispatches amoonkale-themeevent.ws.shell.theme_lighttells Rust code (the native code editor) whether the current theme is light. - Editors: CodeMirror builds its theme from the tokens (
packages/js/codemirror/src/theme.ts,themeFromTokens), xterm reads the CSS variables and re-themes on thedata-themechange, the graph renderer takes its clear and label colours throughGraphView::set_theme. Milkdown, tables, the flow canvas and KaTeX use the tokens directly in CSS. - Theme files: JSON,
{"name": "Paper", "base": "light", "tokens": {"canvas": "#fbf8f1", "accent": "#8a5a00"}}— the base’s palette with the named tokens overridden; unknown names are ignored, values that could close a CSS rule (; { } < >) are dropped. Sources:<config>/moonkale/themes/*.json(desktop and mobile read their own; the web client asks the server,POST /api/themes, via theThemeFilesservice) andExtension::themes(). Their CSS goes into a separate#mk-theme-extrastyle element.
Fonts
The font stacks end in "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei" so Chinese renders where one of them is installed. A machine without any CJK font shows boxes (tofu) — on Arch install noto-fonts-cjk (see Development).
Tests
packages/shellunit tests: theme parsing, thesafefilter, the CSS blocks, the workbench templates keeping their{title}/{panel}placeholders.- E2E
appearance.mjs: a German browser locale gives German labels; switching to Chinese and English live; Light tokens and computed colours; Follow system withemulateMedia; aPapertheme file in the config dir appears and applies.