D29 · Indonesian-first text is built in apps/web (option A1)
Accepted · 2026-10-07 · zuki · Closes OQ17 · Detail: the Frontend Engineer's OQ17 input, option A1
Decision: Every string the user sees is written as t('module.screen.element', 'Indonesian default'). The catalog is plain TypeScript in an apps/web folder behind its own alias, with no library. A script collects every call into the Indonesian catalog; English sits in a second file under the same keys; CI fails when one key has two defaults or the catalog is out of date. Until a key has English, t() shows the Indonesian default. Enum labels are a separate, exhaustive record per language over the generated enum types, so a new enum value fails typecheck until both labels exist; badge tones are a separate map with no language. The language comes from profiles.locale, with an opleet-locale cookie as the per-request fast path, and server components hand client components one language only. One mapper in apps/web turns a database refusal into text from structured fields only: a known key, a known SQLSTATE class, or error.unknown with a Sentry event that never carries the details.
Master data with two names: charge_types, lead_sources, inspection_item_types and body_panels, stored as name (Indonesian, required) and name_en (optional, falling back to name); pricing_groups is checked against v1's values first. Proper nouns and legal text keep one name. Indonesian only, with no language parameter: contracts (PKS), BASTK, surat hutang, the public queue page and queue TV, and anything sent to drivers.
Why: apps/web is the only consumer of bilingual text today (D8, D22), and a plain catalog has no dependency and works the same in server components and server actions. It becomes a package when the first Edge Function or backend code renders staff-facing text in the user's language.
Consequences: The check for missing English turns strict module by module, when a module's pages are done. Where the error-key list comes from is OQ24. The Backend Engineer adds name_en to the four tables and the locale RPC (FE-R5).