Skip to content

Interface localization

Typsastra's interface uses a small built-in locale layer. There is no runtime localization dependency. English is the source locale, and English source text is also the message key, so authored labels stay readable and translators can work from extracted strings.

Translate a string

import { t } from "../i18n";

element.textContent = t("Save");
element.title = t`Downloading ${percent}%`;
section.textContent = t("Hello {name}", { name });
  • t("Save") looks the exact source text up in the active catalog.
  • t\Downloading ${percent}%`becomes the keyDownloading {0}%` and interpolates the value after translation.
  • Named values use {name} placeholders. Numbered values from tagged templates use {0}, {1}, and so on. Unknown placeholders are preserved.
  • Missing translations fall back to English.

For count-aware wording, choose the source phrase before translating:

import { tn } from "../i18n";

tn(1, "{count} file", "{count} files", { count: 1 });

Translate authored markup

Static text and the title, placeholder, aria-label, and alt attributes are translated by source text when the app starts:

<button title="Open">Open</button>

Dynamic HTML that contains authored text should be installed with setLocalizedHtml instead of a direct innerHTML assignment:

import { setLocalizedHtml } from "../i18n";

setLocalizedHtml(panel, "<p>No results.</p>");

Explicit keys are available when the authored text is ambiguous or assembled from fragments:

<div data-i18n="Clear recent projects">Clear recent projects</div>
<input data-i18n-placeholder="Search languages..." />
<button data-i18n-title="Open settings" title="Settings"></button>

Use data-i18n-skip on a subtree that must never be translated, such as generated user content. The localizer already skips code, pre, editable regions, shortcut labels, and editor widgets.

Add a locale

A ready-to-copy template with every currently extracted English key is checked in at src/i18n/locales/locale.template.ts. Copy it to a new file, rename the export, set id and label, and translate the values.

Refresh the template after UI strings change:

bun run i18n:template

The template maps each English key to itself, so it is safe to register while translations are incomplete. Remove entries you do not plan to translate; missing keys fall back to English.

  1. Copy src/i18n/locales/locale.template.ts to src/i18n/locales/km.ts.
  2. Rename localeTemplate to khmerLocale.
  3. Set id: "km" and label: "ខ្មែរ".
  4. Translate the catalog values.
  5. Register the locale in src/i18n/locales/index.ts:
import type { LocaleDefinition } from "../types";
import { englishLocale } from "./en";
import { khmerLocale } from "./km";

export const builtinLocales: readonly LocaleDefinition[] = [
  englishLocale,
  khmerLocale,
];

export { englishLocale };
  1. Open Settings → Language → Interface language and choose the locale. The choice is stored in localStorage under typsastra.ui-locale.

setLocale("km") is also available from code. The settings panel re-renders after a change; other surfaces that build labels with t() pick up the new locale the next time they render.

For a JSON key list instead of the TypeScript template, run bun run i18n:extract.

Notes

  • English text remains in the templates, just as with gettext-style catalogs. This keeps diffs small and preserves useful source context.
  • Static markup is matched after whitespace normalization. Mixed-content paragraphs may need data-i18n or a small markup adjustment before translation reads naturally.
  • Do not translate user document text, file paths, compiler diagnostics, or generated code. Those values flow through the UI untouched.