- resolve-destination.ts: reads ?dest= (URL, primary — the inline web_app-button carrier) then initData's start_param (the signed string, secondary — the direct-link/home-screen carrier), ONE slug validation, dest_ prefix stripped; resolves against the webview's own build-time @araharah/search copy; honest null for unknown ids; module-scope memory-only holder (GEO-03/D-07) - destination-card.ts: the confirmation + honest-unavailable cards, createElement/textContent only, existing theme classes (T-04-32) - webapp.ts: TelegramWebAppLike gains startParam, addToHomeScreen?, checkHomeScreenStatus? + HomeScreenStatus (Bot API 8.0+ members) - telegram-mock.ts: satisfies the extended contract in the same change (startParam initData wiring, homeScreenStatus, __homeScreenAdded) - strings.ts: 5 new copy fields in en+ms; web depends on @araharah/search - DP1-DP12 green (45/45 web suite), web:typecheck green
146 lines
6.3 KiB
TypeScript
146 lines
6.3 KiB
TypeScript
/*
|
|
* 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;
|
|
destinationHeadline: string;
|
|
destinationUnavailableHeadline: string;
|
|
destinationUnavailableBody: string;
|
|
addToHomeScreen: string;
|
|
homeScreenAlreadyAdded: string;
|
|
}
|
|
|
|
export type Locale = "en" | "ms";
|
|
|
|
export const strings: Record<Locale, ShellStrings> = {
|
|
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",
|
|
destinationHeadline: "Destination",
|
|
destinationUnavailableHeadline: "Destination unavailable",
|
|
destinationUnavailableBody: "That place is not in this build. Search again from the bot.",
|
|
addToHomeScreen: "ADD TO HOME SCREEN",
|
|
homeScreenAlreadyAdded: "Already on your home screen",
|
|
},
|
|
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",
|
|
destinationHeadline: "Destinasi",
|
|
destinationUnavailableHeadline: "Destinasi tidak tersedia",
|
|
destinationUnavailableBody: "Tempat itu tiada dalam binaan ini. Cari semula dari bot.",
|
|
addToHomeScreen: "TAMBAH KE SKRIN UTAMA",
|
|
homeScreenAlreadyAdded: "Sudah di skrin utama anda",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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=<url-encoded JSON>&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";
|
|
}
|
|
}
|