# Markets (Manage)

**Nav:** Manage sidebar → **Setting** → **Markets** (`/manage/markets`).
Gated on `VIEW_INTEGRATIONS`; toggling needs `MANAGE_INTEGRATIONS`.

## What it does

Decides which countries this deployment serves publicly. One checkbox per
country over `countries.is_active` — tick to serve the market, untick to hide
it. **Nothing is deleted**: the catalogue rows, media, floor plans and saved
analyses stay exactly as they are and return the moment the market is ticked
again.

## How it works

- **One flag, whole-site reach.** `App\Http\Middleware\SetPublicSite` resolves
  the URL country from **active countries only** and `abort_unless($country,
  404)`. Every public page lives inside the `{country}` route group behind that
  middleware, so unticking a market 404s its entire site in one move — home,
  `new-projects`, every project detail page, the region switcher. `SitemapController`
  and `SiteController`'s country tabs filter on the same flag, so a hidden
  market is not advertised either.
- **The portal reads catalogue rows regardless, but its LINKS must serve.** The
  only portal surface that lists catalogue projects is Analyze Property → New
  Project, and every card links at the **public** detail page
  (`/{iso2}/projects/{slug}`) — which SetPublicSite 404s for a hidden market. So
  that listing follows an active market (default when served, else the first
  active one), while the data-only fallbacks — the project typeahead, the
  `country_id` default on portal writes and catalogue imports — keep using
  `Country::defaultCountry()` whether or not it is served publicly.
- **The default market is switchable like any other.** Hiding it does not break
  the root `/`: SetPublicSite resolves the rootless URL from **active** markets
  only — visitor cookie → an active default → the first active market by name —
  so the front door simply moves to another market. `SiteController::region`
  picks its no-`?from=` landing the same way, for the same reason. The confirm
  modal names the market the front door moves to before you click.
  `is_default` keeps its **other** job while hidden: it is still the `country_id`
  fallback for portal writes, catalogue imports and Analyze Property, all of
  which read the row regardless of whether it is served publicly. So the badge
  is *default* (fallback market), not *locked*.
- **The LAST active market cannot be switched off** (`CountryRepository::setActive`
  throws; the controller flashes the error and returns `back()`). With no active
  market the root has no country to resolve and `abort_unless($country, 404)`
  takes the entire public site down, not one market's.
- **Switching OFF confirms, switching ON does not.** Taking a public site down
  is the destructive direction; the confirm names the market and how many
  published projects stop serving. There is no client-side guard on the last
  market — the server throws and flashes, so the rule lives in one place.
- Each row shows currency + timezone, published-of-total project counts, and a
  Visit link to that market's live home page.

### Traditional Chinese on the Hong Kong hostname (2026-09-04)

Three rules make `starcityglobal.ai` read as a Traditional-Chinese site end to end, at the
founder's request ("a website for HK people, so it should be a traditional chinese website"):

- **A hostname can PIN its language** — `market_sites.locale`, the **Language** field on a site
  rule (Setting → Markets → domain rules; migration 2026-09-04). When set, `SetPublicSite` and
  `SetLocaleTimezone` use it and ignore `?lang`, the `site_lang` cookie and the session, on the public
  site and the portal alike, and the page gets `site.lockedLocale = true` (the home page's region modal
  stays closed). The default rule that serves `starcityglobal.ai` is pinned to `zh_HK`. WHY a pin and
  not just a default: the site's header no longer has a switcher, so a cookie from the old dropdown
  would have stranded a reader in English with no way back — which is exactly what the founder saw.
- **The hostname's landing market also decides the PORTAL's default locale.** `SetPublicSite`
  already used `market_sites.default_country_id` for the public site; `SetLocaleTimezone` (every
  `web` request, so the portal too) now falls back to that market's `countries.locale` before
  `app.locale`. A member who signs in on the Hong Kong hostname without ever opening the marketing
  site still gets `zh_HK`; the Malaysian hostnames land on English and are unaffected.
- **The public site's `t()` copy is fully in `resources/lang/zh_HK.json`** — 244 project-detail,
  projection and amenity strings that had no entry (and so rendered in English) were added that day.
  The public header has **no language / currency switcher** on this site any more; the
  `/region/{key}` route and the presets still exist server-side for the other hostnames.
- **The portal's hand-written copy converts at runtime.** The lessons, courses, road, and
  Investment Prompt plays are Simplified Chinese written straight into JS/PHP (~28,000 characters,
  213 files) and shared with the Malaysian sites, so they are not forked: under `lang="zh-HK"`,
  [`resources/js/utils/zhHk.js`](/resources/js/utils/zhHk.js) lazy-loads OpenCC's `cn → hk`
  converter (its own ~500 KB gzipped chunk, never loaded for other locales) and converts every text
  node and reader-facing attribute, on load and on each DOM patch. Inputs' values, `<code>`, and
  anything inside `[data-no-zh-convert]` are left alone. Tested in `zhHk.test.js`.

## Related files

- `app/Http/Controllers/Manage/Setting/MarketsController.php`,
  `app/Http/Requests/Manage/Setting/UpdateMarketRequest.php`
- `src/Common/Repositories/CountryRepository.php` (`setActive`), `src/Common/Country.php`
- `resources/js/Pages/Manage/Setting/Markets/Index.vue`, `Components/SettingTabs.vue`
- `app/Http/Middleware/SetPublicSite.php` — the middleware the flag actually drives
- `app/Http/Controllers/Main/SiteController.php` (`region`) — its fallback landing
  must be an active market too
- `tests/Feature/Manage/Setting/MarketsTest.php`
