{"version":3,"sources":["../src/useLocale.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\n/**\n * Locale resolved for the current visitor.\n *\n * Derived from a single source signal — Cloudflare's edge-injected\n * country code at `/cdn-cgi/trace`. Same hook drives currency display\n * today and language switching tomorrow.\n *\n * `country`  ISO 3166-1 alpha-2, uppercase. `null` while resolving;\n *            `'XX'` if we can't determine geo.\n * `currency` `'IDR'` for Indonesia, `'USD'` for everyone else. Defaults\n *            to `'IDR'` during the resolving phase to match the\n *            statically-rendered first paint on Indonesian-first sites.\n * `lang`     `'id'` for Indonesia, `'en'` otherwise. (Wired for future\n *            translation work; no translations land in this version.)\n * `resolved` `false` while the geo fetch is in flight; `true` once\n *            the values above are final for this session.\n */\nexport interface Locale {\n  country: string | null;\n  currency: 'IDR' | 'USD';\n  lang: 'en' | 'id';\n  resolved: boolean;\n}\n\nconst SESSION_KEY = 'forjio.locale.country.v1';\n\nfunction deriveCurrency(country: string | null): 'IDR' | 'USD' {\n  return country === 'ID' ? 'IDR' : 'USD';\n}\n\nfunction deriveLang(country: string | null): 'en' | 'id' {\n  return country === 'ID' ? 'id' : 'en';\n}\n\n/**\n * Resolve a country code from the browser without a network round-trip.\n *\n * Primary signal: **timezone** (`Intl.DateTimeFormat().resolvedOptions().timeZone`).\n * Indonesian devices are set to `Asia/Jakarta` / `Asia/Pontianak` /\n * `Asia/Makassar` / `Asia/Jayapura` (one per WIB/WITA/WIT zone) regardless\n * of UI language — and VPN doesn't change the timezone. This handles the\n * common case where an Indonesian user has en-US locale but is physically\n * in Indonesia (which broke the language-only detection).\n *\n * Fallback: `navigator.language` region subtag (`id-ID` → ID, `en-US`\n * → US). Used when timezone is missing or unusual (some embedded\n * webviews don't expose Intl).\n *\n * Why not `/cdn-cgi/trace` (the original signal): non-Cloudflare staging\n * environments 404 on it but the pending request blocks Playwright's\n * `waitUntil: 'networkidle'` for 45s. Locale + timezone are synchronous\n * and Cloudflare-independent.\n */\nconst INDONESIAN_TIMEZONES = new Set([\n  'Asia/Jakarta',     // WIB — Sumatra, Java, West/Central Kalimantan\n  'Asia/Pontianak',   // WIB — West Kalimantan\n  'Asia/Makassar',    // WITA — Sulawesi, Bali, Nusa Tenggara, East/South Kalimantan\n  'Asia/Jayapura',    // WIT — Maluku, Papua\n]);\n\nfunction resolveCountryFromBrowser(): string {\n  // Primary: timezone — survives VPN, ignores UI language preference.\n  try {\n    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;\n    if (tz && INDONESIAN_TIMEZONES.has(tz)) return 'ID';\n    // For non-ID timezones, we don't try to map every tz → country code\n    // (there are 400+). We just know \"not Indonesian\" → fall through to\n    // language for the precise region.\n  } catch {\n    // Some embedded webviews don't expose Intl — fall through.\n  }\n\n  if (typeof navigator === 'undefined') return 'XX';\n  const tags: string[] = [];\n  if (navigator.language) tags.push(navigator.language);\n  if (Array.isArray(navigator.languages)) tags.push(...navigator.languages);\n  for (const tag of tags) {\n    const region = tag.split('-')[1];\n    if (region && /^[A-Z]{2}$/.test(region)) return region.toUpperCase();\n  }\n  if (tags.some((t) => t.toLowerCase().startsWith('id'))) return 'ID';\n  return 'XX';\n}\n\n/**\n * Client-side locale hook. Server-renders as `{ resolved: false }` with\n * the IDR/id defaults so the first paint matches the Indonesian-first\n * default; once the hook resolves on the client, components re-render\n * with the actual currency / language for the visitor.\n *\n * Consumers should bias their static markup toward the IDR defaults\n * (no flicker for Indonesian visitors, brief swap-in for international).\n */\nexport function useLocale(): Locale {\n  // null = pre-hydration (SSR + first client render). Falls back to the\n  // Indonesian-first defaults so SSR HTML matches first paint for\n  // Indonesian visitors (zero flicker). useEffect resolves it on the\n  // client immediately after hydration.\n  const [country, setCountry] = useState<string | null>(null);\n\n  useEffect(() => {\n    // sessionStorage override wins (used by setLocaleCountry below).\n    try {\n      const cached = sessionStorage.getItem(SESSION_KEY);\n      if (cached) { setCountry(cached); return; }\n    } catch {\n      // sessionStorage can throw in incognito-like modes; fall through.\n    }\n    setCountry(resolveCountryFromBrowser());\n  }, []);\n\n  if (country === null) {\n    return { country: null, currency: 'IDR', lang: 'id', resolved: false };\n  }\n  return {\n    country,\n    currency: deriveCurrency(country),\n    lang: deriveLang(country),\n    resolved: true,\n  };\n}\n\n/** Imperative override for the resolved country. Useful for a manual\n *  \"View in: IDR / USD\" toggle the user can flip from the footer or\n *  account menu. Writes through sessionStorage so it survives nav. */\nexport function setLocaleCountry(country: string): void {\n  try { sessionStorage.setItem(SESSION_KEY, country.toUpperCase()); } catch { /* ignore */ }\n  // Force a reload so every Price across the page re-reads. Cheaper\n  // than wiring a pub/sub for a feature that fires once per visit.\n  if (typeof window !== 'undefined') window.location.reload();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAAoC;AA0BpC,MAAM,cAAc;AAEpB,SAAS,eAAe,SAAuC;AAC7D,SAAO,YAAY,OAAO,QAAQ;AACpC;AAEA,SAAS,WAAW,SAAqC;AACvD,SAAO,YAAY,OAAO,OAAO;AACnC;AAqBA,MAAM,uBAAuB,oBAAI,IAAI;AAAA,EACnC;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF,CAAC;AAED,SAAS,4BAAoC;AAE3C,MAAI;AACF,UAAM,KAAK,KAAK,eAAe,EAAE,gBAAgB,EAAE;AACnD,QAAI,MAAM,qBAAqB,IAAI,EAAE,EAAG,QAAO;AAAA,EAIjD,QAAQ;AAAA,EAER;AAEA,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,QAAM,OAAiB,CAAC;AACxB,MAAI,UAAU,SAAU,MAAK,KAAK,UAAU,QAAQ;AACpD,MAAI,MAAM,QAAQ,UAAU,SAAS,EAAG,MAAK,KAAK,GAAG,UAAU,SAAS;AACxE,aAAW,OAAO,MAAM;AACtB,UAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC;AAC/B,QAAI,UAAU,aAAa,KAAK,MAAM,EAAG,QAAO,OAAO,YAAY;AAAA,EACrE;AACA,MAAI,KAAK,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC,EAAG,QAAO;AAC/D,SAAO;AACT;AAWO,SAAS,YAAoB;AAKlC,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAwB,IAAI;AAE1D,8BAAU,MAAM;AAEd,QAAI;AACF,YAAM,SAAS,eAAe,QAAQ,WAAW;AACjD,UAAI,QAAQ;AAAE,mBAAW,MAAM;AAAG;AAAA,MAAQ;AAAA,IAC5C,QAAQ;AAAA,IAER;AACA,eAAW,0BAA0B,CAAC;AAAA,EACxC,GAAG,CAAC,CAAC;AAEL,MAAI,YAAY,MAAM;AACpB,WAAO,EAAE,SAAS,MAAM,UAAU,OAAO,MAAM,MAAM,UAAU,MAAM;AAAA,EACvE;AACA,SAAO;AAAA,IACL;AAAA,IACA,UAAU,eAAe,OAAO;AAAA,IAChC,MAAM,WAAW,OAAO;AAAA,IACxB,UAAU;AAAA,EACZ;AACF;AAKO,SAAS,iBAAiB,SAAuB;AACtD,MAAI;AAAE,mBAAe,QAAQ,aAAa,QAAQ,YAAY,CAAC;AAAA,EAAG,QAAQ;AAAA,EAAe;AAGzF,MAAI,OAAO,WAAW,YAAa,QAAO,SAAS,OAAO;AAC5D;","names":[]}