Languages and theming in an app
An app translates its own texts: the desktop hands it the user’s language, formats and time zone in app.context and says when they change. @pithyx/sdk/i18n is an optional helper for ICU MessageFormat texts, the format the desktop uses itself, and app.format formats dates, numbers, sizes and lists. The look comes as CSS custom properties --pithyx-* that the SDK sets on the page and keeps current, so plain CSS follows light and dark, the accent color and the density.
Example
Section titled “Example”The texts live in one JSON file per language:
{ "files": { "title": "Files", "count": "{count, plural, one {# file} other {# files}}", "changed": "Changed {when}" }}{ "files": { "title": "Dateien", "count": "{count, plural, one {# Datei} other {# Dateien}}", "changed": "Geändert {when}" }}// Texts in the user's language, dates and sizes in the user's formats, and colors from the desktop's theme.import { connect, type PithyxApp } from '@pithyx/sdk';import { createI18n } from '@pithyx/sdk/i18n';// ICU MessageFormat, like the desktop's own texts; English is the fallback for keys a language lacks.import de from './i18n/de.json';import en from './i18n/en.json';
const app: PithyxApp = await connect();const i18n = createI18n(app, { en, de });
interface Row { name: string; size: number; modified: string;}
function render(rows: Row[]) { document.title = i18n.t('files.title'); const heading = document.createElement('h1'); heading.textContent = `${i18n.t('files.title')} (${i18n.t('files.count', { count: rows.length })})`; const list = document.createElement('ul'); for (const row of rows) { const item = document.createElement('li'); // Dates arrive as UTC; app.format shows them in the user's time zone and locale. item.textContent = `${row.name}, ${app.format.bytes(row.size)}, ${i18n.t('files.changed', { when: app.format.relative(row.modified), })}`; list.append(item); } document.body.replaceChildren(heading, list);}
const rows: Row[] = [ { name: 'report.pdf', size: 1_234_567, modified: new Date(Date.now() - 3_600_000).toISOString() },];
render(rows);// The SDK already set the new lang attribute and theme properties when these handlers run.app.on('localeChange', () => { render(rows);});app.on('themeChange', (theme) => { // CSS with var(--pithyx-...) follows by itself; only canvas drawings and the like need the values. document.body.dataset.scheme = theme.scheme;});And the CSS that follows the theme:
body { margin: 0; background: var(--pithyx-color-background); color: var(--pithyx-color-text); font: var(--pithyx-font-size) var(--pithyx-font-ui);}button { height: var(--pithyx-control-height); padding: 0 calc(var(--pithyx-space) * 2); border: 1px solid var(--pithyx-color-border); border-radius: var(--pithyx-radius); background: var(--pithyx-color-accent); color: var(--pithyx-color-accent-text);}button:focus-visible { outline: 2px solid var(--pithyx-color-focus);}Language and formats
Section titled “Language and formats”context.languageis the user’s language for texts, for examplede.context.localeis that language with the region of the box’s formats, for examplede-AT; the SDK sets it aslangon the root element.context.timeZoneis an IANA zone such asEurope/Berlin.createI18n(app, { en, de, ... })needsen. It picks the messages of the exact locale, then of its base language (deforde-AT), thenen, and readscontext.localeon every call, so it followslocaleChangeby itself. A key a language lacks falls back to the English text; a key no language has comes back as the key itself. Keys are dotted paths into nested objects.- Messages are ICU MessageFormat:
{count, plural, one {# file} other {# files}},{name},{kind, select, ...}. Each message is compiled once per locale. app.formathasdate,time,dateTime,relative,number,bytes(decimal units, as the desktop shows sizes) andlist. They use the current locale and time zone. Dates cross the API as UTC ISO 8601 strings; pass them as they are.- Re-render on
localeChange. The SDK has already updated the context andlangwhen your handler runs. - Any other i18n library works too; the SDK only hands over the locale.
The SDK sets these properties on document.documentElement (or the themeRoot you pass to connect, null to skip):
- colors:
--pithyx-color-background,-surface,-surface-raised,-hover,-selected,-text,-text-muted,-border,-accent,-accent-text,-focus,-success,-warning,-danger, - type and shape:
--pithyx-font-ui,--pithyx-font-mono,--pithyx-font-size,--pithyx-radius,--pithyx-space,--pithyx-control-height.
It also sets color-scheme, data-theme and data-pithyx-theme (both light or dark) and data-pithyx-density (comfortable or compact; compact already shrinks --pithyx-space and --pithyx-control-height). On themeChange the properties change before your handler runs; CSS follows without code, also when your bundler compiled light-dark() for older browsers (Vite does: the compiled CSS follows data-theme and the browser’s setting, not a color-scheme set by script, so do not remove data-theme). Only drawings on a canvas or in WebGL need the values from theme.tokens.
For ready components in the desktop’s look, see @pithyx/ui; they follow the same theme by themselves once the SDK connected.
Edge cases
Section titled “Edge cases”- Fonts. The font properties name the desktop’s fonts, but the app’s page does not share the fonts the desktop loaded and may load fonts only from its own files or
data:URLs (font-src 'self' data:). Ship the font files with your frontend if you rely on them; otherwise the browser falls back to another font. - Unknown values. A token value with
;,{,},<or>is never set, so nothing from the host can inject CSS. - Mock desktop.
pithyx devswitches theme, accent, locale (en-US,en-GB,de-DE,de-AT,fr-FR,ja-JP), time zone and size class from its panel. Its density is alwayscomfortable.
Limits
Section titled “Limits”Names of the --pithyx-* properties are a stable contract: new names are a minor change, renaming or removing one is a major change. Raw colors are never part of it; use the names by purpose.