ThemeToggle + LanguageSwitcher — small utility modules
Two tiny modules grouped together because each is one palette command plus a callback. ThemeToggle owns the
data-themeattribute on<html>; LanguageSwitcher owns nothing — it just calls your i18n layer when the user picks a language.
Both opt-in. Both register palette commands via .registerOn(palette).
When to use
- ThemeToggle: any app that wants light / dark / system without writing the persistence + system-preference-listener glue
- LanguageSwitcher: any app whose i18n layer already exists and you just need a discoverable way to switch locales from the palette
Don't use when:
- You already ship a theme toggle in your own header — call
document.documentElement.dataset.theme = 'dark'directly, no module needed - Your locale list changes per-user (e.g. only the languages the user has bought a subscription for) — wrap the module yourself or just register the palette items manually
ThemeToggle
Import
import { ThemeToggleModule } from '@perhapxin/dddk';
import type { ThemeToggleConfig } from '@perhapxin/dddk';
Minimum setup
import { ThemeToggleModule } from '@perhapxin/dddk';
const theme = new ThemeToggleModule({
storage: localStorageAdapter, // optional but recommended
onChange: (val) => console.log('theme is now', val),
});
theme.apply(); // apply stored preference (or 'system')
theme.registerOn(dddk.palette); // adds /theme commands
Three palette commands appear: 主題:亮色 / 主題:暗色 / 主題:跟隨系統.
Configuration
| Option | Default | Description |
|---|---|---|
storage |
— | StorageAdapter. If unset, preference is forgotten on reload. |
attribute |
'data-theme' |
Attribute to set on <html>. |
storageKey |
'dddk.theme' |
Storage key. |
onChange |
— | (theme: 'light' | 'dark' | 'system') => void. |
Runtime API
theme.apply('dark'); // set and persist
theme.apply(); // re-apply current preference (useful after hydration)
theme.read(); // current preference: 'light' | 'dark' | 'system'
theme.toggle(); // light → dark → system → light → ...
System mode
When the preference is 'system', ThemeToggleModule subscribes to matchMedia('(prefers-color-scheme: dark)') and re-applies on changes. Selecting 'light' or 'dark' explicitly unsubscribes — the user's override wins.
Or just set the attribute
If you don't need the palette commands:
document.documentElement.dataset.theme = 'dark';
All dddk surfaces re-render via CSS tokens. See ../theming.md for the full token reference plus custom modes (sepia, high-contrast).
LanguageSwitcher
Import
import { LanguageSwitcherModule } from '@perhapxin/dddk';
import type { LanguageSwitcherConfig } from '@perhapxin/dddk';
Minimum setup
import { LanguageSwitcherModule } from '@perhapxin/dddk';
const lang = new LanguageSwitcherModule({
available: [
{ code: 'en', label: 'English' },
{ code: 'zh-TW', label: '繁體中文' },
{ code: 'ja', label: '日本語' },
],
current: 'en',
storage: localStorageAdapter,
onChange: (code) => yourI18n.setLocale(code),
});
lang.registerOn(dddk.palette); // adds /language commands
One palette command per language. The current language gets a ✓ 目前 annotation in the palette.
Configuration
| Option | Default | Description |
|---|---|---|
available |
— | Array<{ code, label }>. Required. |
current |
navigator.language |
Starting language. |
storage |
— | StorageAdapter. Persists the picked code. |
storageKey |
'dddk.language' |
Storage key. |
onChange |
— | (code) => void. Required. |
Runtime API
lang.getCurrent(); // current code
lang.switch('ja'); // programmatically switch (fires onChange)
dddk does NOT own your i18n
LanguageSwitcherModule is just a palette-command + persistence layer. Your app is responsible for actually applying the locale — onChange is the seam. Common wirings:
// next-intl
onChange: (code) => router.replace({}, { locale: code });
// sveltekit-i18n
onChange: (code) => locale.set(code);
// vue-i18n
onChange: (code) => (i18n.global.locale.value = code);
The dddk built-in modules (Voice, InlineAgent, ImmersiveTranslate) have their own locale config — LanguageSwitcher does not push to them automatically. Mirror the change yourself if you need:
onChange: (code) => {
yourI18n.setLocale(code);
inline.setActions(buildActionsFor(code)); // example
};
Tear-down
Neither module holds any DOM. ThemeToggleModule unsubscribes from matchMedia automatically when you switch away from 'system'. To fully detach you can recreate the palette without their commands, or call palette.removeItem(id) for each.
See also: ../theming.md for the CSS-token system ThemeToggle drives, ./overview.md for the module index.