Skip to content

Internationalization

Not a UI-only rule — it covers the capture extension and server-side notification text too.

ui/ and extension/ are each fully internationalized with Paraglide JS (EN + DE), with separate catalogs, separate compiled output, and separate gates. Never hardcode user-facing text. Every display string — labels, buttons, placeholders, title/aria-label, empty/error/loading states, toast text, and server-side notification payloads — must route through a message.

Write to the catalog of the package you are editing — they are not shared, and a key added to the wrong one simply never resolves:

Editing Catalogs (add the key to both locales) Import Gate
ui/src/** ui/messages/en.json + de.json import { m } from "$lib/paraglide/messages" cd ui && bun run check:i18n
extension/src/** extension/messages/en.json + de.json relative, e.g. import { m } from "../lib/paraglide/messages" cd extension && bun run check:i18n

Then: m.my_key() / m.my_key({ count }). Keys are snake_case and component-prefixed (viewport_diff_tab, broadcast_failed, prbadge_open); use {param} for interpolation. Reuse an existing key where one fits (e.g. common_close, common_loading).

Data passed through verbatim (tool-use summaries, PR titles, designations like TASK-07) is not translated — only chrome the app itself authors.

Server-side notification text (no gate covers this)

Section titled “Server-side notification text (no gate covers this)”

Push notification strings live in the locale-keyed NOTIFY_TEXT table (en + de) in src/push.ts, read by blockSummary() and buildPayload(); src/ready-notify.ts composes the NotifyInput that reaches them. A new notification kind or reason adds its strings to NOTIFY_TEXT, under every locale — never as a literal at the call site.

This table is deliberately separate from the Paraglide catalog: the server package can’t reach ui/’s compiled messages. Don’t try to import them here — keep EN+DE in sync by hand.

Each check:i18n resolves its message directory to its own package’s messages/, so neither gate can see this table. Nothing will fail if you hardcode here; this rule and review are the only enforcement.

Run the one for the package you touched (both, if the change spans them) — see the table above. Each asserts that its package’s locale catalogs share an identical, non-empty key set. Paraglide silently falls back to EN for a missing key, so an incomplete de.json would otherwise ship looking fine.

Neither gate detects hardcoded strings that skip the catalog entirely, and neither reads the other package’s catalog; that’s on you and review.