/* * strings.ts — the Mini App shell's copy tokens.css (UI-SPEC §Copywriting Contract). * ------------------------------------------------------------------------------- * Every user-facing string the shell renders lives here and NOWHERE else — * `web/src/ui/*` and `web/src/main.ts` import from this module, and the copy * gate (web/test/copy-gate.test.mjs COPY1..COPY5) keeps it verbatim against * the approved UI-SPEC table in en + ms. Register labels (`BOOT-01 //`, * `BOOT-02 //`, `GEO // LOCATION`, `SYS // POSITION`) are instrument * vocabulary: identical in both locales by design. Tone is clinical + honest * (D-01): plain statements of what is stored and what is not. * * Locales: English default ("en") + Bahasa Melayu ("ms") — Phase 2 D-05 * precedent, same pickLocale ms-prefix rule as bot/src/i18n/strings.ts. */ export interface ShellStrings { bootValidatingRegister: string; bootValidatingHeadline: string; bootValidatingBody: string; bootAcquiringRegister: string; bootAcquiringHeadline: string; bootAcquiringBody: string; deniedRegister: string; ctaTryAgain: string; ctaAskViaChat: string; ctaLocationSettings: string; deniedHeadline: string; deniedBody: string; deniedHint: string; lostRegister: string; lostHeadline: string; lostBody: string; lostHint: string; chatFallbackWaiting: string; stripValidating: string; stripAcquiring: string; stripLive: string; stripSignalLost: string; stripLocationOff: string; stripAwaitingPin: string; attribution: string; ariaRecenter: string; } export type Locale = "en" | "ms"; export const strings: Record = { en: { bootValidatingRegister: "BOOT-01 // SESSION", bootValidatingHeadline: "Validating session", bootValidatingBody: "Confirming your Telegram identity. Takes a few seconds and stores nothing.", bootAcquiringRegister: "BOOT-02 // LOCATION", bootAcquiringHeadline: "Acquiring location", bootAcquiringBody: "Finding where you are. Your position is never written to storage.", deniedRegister: "GEO // LOCATION", ctaTryAgain: "TRY AGAIN", ctaAskViaChat: "ASK VIA CHAT", ctaLocationSettings: "LOCATION SETTINGS", deniedHeadline: "Location is switched off", deniedBody: "araharah guides you from where you physically are. Without location there is no route to draw.", deniedHint: "Grant location to Telegram, or send a pin in this chat: attach → Location.", lostRegister: "SYS // POSITION", lostHeadline: "Signal lost", lostBody: "Showing your last known position — marked on the map. Reacquiring automatically.", lostHint: "You can also send a fresh pin in this chat: attach → Location.", chatFallbackWaiting: "Waiting for the pin you send in chat.", stripValidating: "VALIDATING", stripAcquiring: "ACQUIRING", stripLive: "LIVE", stripSignalLost: "SIGNAL LOST", stripLocationOff: "LOCATION OFF", stripAwaitingPin: "AWAITING PIN", attribution: "© OpenStreetMap contributors", ariaRecenter: "Recenter map", }, ms: { bootValidatingRegister: "BOOT-01 // SESSION", bootValidatingHeadline: "Menyemak sesi", bootValidatingBody: "Mengesahkan identiti Telegram anda. Hanya beberapa saat, tiada apa-apa disimpan.", bootAcquiringRegister: "BOOT-02 // LOCATION", bootAcquiringHeadline: "Mendapatkan lokasi", bootAcquiringBody: "Mencari kedudukan anda. Lokasi anda tidak pernah disimpan.", deniedRegister: "GEO // LOCATION", ctaTryAgain: "CUBA LAGI", ctaAskViaChat: "MINTA VIA CHAT", ctaLocationSettings: "TETAPAN LOKASI", deniedHeadline: "Lokasi dimatikan", deniedBody: "araharah memandu dari kedudukan sebenar anda. Tanpa lokasi, tiada laluan boleh dilukis.", deniedHint: "Benarkan lokasi untuk Telegram, atau hantar pin dalam chat ini: lampiran → Lokasi.", lostRegister: "SYS // POSITION", lostHeadline: "Isyarat hilang", lostBody: "Menunjukkan kedudukan terakhir anda — ditanda pada peta. Sedang mencari semula.", lostHint: "Anda juga boleh hantar pin baharu dalam chat ini: lampiran → Lokasi.", chatFallbackWaiting: "Menanti pin yang anda hantar dalam chat.", stripValidating: "MENYEMAK", stripAcquiring: "MENDAPAT", stripLive: "LANGSUNG", stripSignalLost: "ISYARAT HILANG", stripLocationOff: "LOKASI MATI", stripAwaitingPin: "AWAITING PIN", attribution: "© OpenStreetMap contributors", ariaRecenter: "Tetapkan semula peta", }, }; /** * D-05 fallback rule (verbatim from bot/src/i18n/strings.ts): a Telegram * `language_code` that starts with the Malay tag selects "ms"; every other * value (including null/undefined and unknown languages) falls back to en. */ export function pickLocale(telegramLanguageCode?: string | null): Locale { const code = telegramLanguageCode ?? ""; return code === "ms" || code.startsWith("ms-") ? "ms" : "en"; } /** Derive the locale from the RAW initData string (RESEARCH §Security V2 — * the signed string, never initDataUnsafe). Telegram's wire format is * user=&auth_date=...&query_id=...&signature=...&hash=..., * so the first query component is `user`. A malformed or absent field * falls back to "en" — never throws, never blocks boot. */ export function localeFromInitData(initData: string): Locale { try { const userField = initData.split("&")[0] ?? ""; if (!userField.startsWith("user=")) return "en"; const user = JSON.parse(decodeURIComponent(userField.slice(5))) as { language_code?: string }; return pickLocale(user.language_code); } catch { return "en"; } }