// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /languages/ · LanguagesController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Languages"
/* ====================================================================
   LANGUAGES — Settings ▾ rebuild (Batch 3), Hrs* shell
   ====================================================================
   Three surfaces behind one sidebar entry, exactly as the real platform
   splits them (list → translate grid → raw-PHP editor). This file loads
   after HostSettings.jsx, so its `SetLanguages` replaces the legacy stub.

   Traceability (all line cites are LanguagesController.php unless noted):
   · Page      GET /languages/                       routes/admin.php:1328-1330 · index:52
               (unnamed route — array syntax, so the surrounding
               Route::name('admin.') prefix attaches no name)
   · Rows      GET /languages/getLanguagesTable      admin.php:1332 · :419
   · Form      GET /languages/form/                  admin.php:1336 · languageForm:517
   · Save      POST /languages/saveLanguage/         admin.php:1340 · saveLanguage:667
   · Translate GET /languages/translate/{lang_id}    admin.php:1344 · showTransalte:128
               (method-name typo is in the source)
   · Save tr.  POST /languages/saveTranslation/{id}  admin.php:1347 · :545
   · Code      GET /languages/code/{lang_id}         admin.php:1350 · showCode:170
   · Save code POST /languages/saveCode/{id}         admin.php:1353 · :621
   · Seed      GET /languages/generateDefaultLang/   admin.php:1356 · :102
   · Delete    GET /languages/delete/{id}/           admin.php:1608 · delete:731
   · Google    GET /translateByGoogle/               admin.php:1656 · :317
               POST /translateByGoogleBatch/         admin.php:1660 · :353
   · Blade     admin/languages/index.blade.php · forms/language.blade.php ·
               translate.blade.php · code.blade.php · JS public/js/pages/languages/ajax.js

   PERMISSION HONESTY (surfaced in the page Tip, not hidden): the sidebar
   entry renders only for `isadmin()` (SUPER_ADMIN, user_level 0 —
   sidebar.blade.php:522, utils.php:528) inside a Settings ▾ dropdown gated
   `isadmin() || isSkinAdmin() || $enable_agents_operators`. Server side ONLY
   `delete()` re-checks isadmin() (:732). saveLanguage / showTransalte /
   saveTranslation / showCode / saveCode / generateDefaultLang carry NO role
   check at all — any authenticated 2FA'd BO user of any level can reach them
   by URL, and saveCode writes arbitrary PHP to disk.
   // <!-- SUGGESTION: add isadmin() to saveLanguage, showTransalte,
   //      saveTranslation, showCode, saveCode and generateDefaultLang. Today
   //      the only thing keeping a Cashier out of an arbitrary-PHP-write
   //      endpoint is that the sidebar does not show them the link. -->

   Faithful absences (nothing added — brief §3): no KPIs, no export, no bulk
   actions, no per-row status column (the list is ID / Name / Translate /
   Actions and nothing else, controller L57), no create/update audit columns
   (`addedByUser`/`updatedByUser` are not fillable and are never written).
   `LanguagesController::stati()` (:408 — 0 "Disabled" / 1 "Active") and
   `tipologieLanguages()` (:503) are dead code on this screen and are not
   used here either.
   // <!-- SUGGESTION: the list hides `backend`/`frontend` even though
   //      `backend` decides whether a language is accepted by the admin
   //      language picker (UsersController:4887) — an operator cannot tell
   //      an enabled language from a disabled one without opening the form.
   //      stati() already exists for exactly this rendering. -->
   // <!-- SUGGESTION: `novus_ok` gates whether a player's language is passed
   //      to Novus / CmsWager / IGPixel / MondoGaming game launches, and
   //      `bcw_ok` / `sa_ok` exist in the migration with no consumer — none
   //      of the three has a form field, so they can only be changed by
   //      direct SQL. Either surface them or drop the dead ones. -->

   Known-bug divergences — evident intent implemented (CLAUDE.md policy):
   1. Edit form prefills `flagicon` with `$row->locale`
      (forms/language.blade.php:48). Here the field shows the row's real
      flagicon.
      // <!-- SUGGESTION: fix forms/language.blade.php:48 to echo
      //      $row->flagicon — today every edit silently rewrites the flag
      //      icon to the locale string the moment the operator saves. -->
   2. Reset: ajax.js binds a `#kt_reset` handler to an element the blade
      never renders, so filters can only be cleared by hand. A working Reset
      is wired here.
      // <!-- SUGGESTION: render the #kt_reset button index.blade.php already
      //      has a handler for. -->
   3. The `Translate` column header is clickable but the server order switch
      only knows `id` and `name` (:449-467), so clicking it silently falls
      back to `languages.id ASC`. The cell renders `languages.name`, so here
      the header sorts by name.
      // <!-- SUGGESTION: map the Translate column to `name` in the order
      //      switch (or mark it orderable:false) — today it looks sortable
      //      and quietly reorders by id instead. -->
   4. Nested-key display bug: for array-valued lang entries the target-side
      textarea is prefilled with the EN source value, not the target's own
      sub-translation (translate.blade.php:164-183) — so re-saving reverts
      nested translations to English. Here nested rows show the target's own
      saved value.
      // <!-- SUGGESTION: fix translate.blade.php:164-183 to read the
      //      sub-key from $lang_data_to; today opening a translate page and
      //      pressing Save is enough to wipe every nested translation. -->
   5. `saveTranslation`'s `str_replace("'", "'", $val)` is a no-op (it
      replaces an apostrophe with itself) and nothing escapes `"`, so a typed
      double quote can corrupt the generated PHP file. This build escapes
      quotes on save and flags affected rows while typing.
      // <!-- SUGGESTION: replace the no-op str_replace with
      //      addslashes()/var_export() when regenerating the file — a single
      //      typed " in a translation currently breaks the whole catalogue
      //      for that language until someone edits the file by hand. -->
   6. `saveLanguage`'s create path discards `Language::create($datip)->id`
      and echoes back the (empty) request id (:719-722). The new row here
      carries its real id.
      // <!-- SUGGESTION: return the created id in the save response. -->
   7. The `Edit php` link on the translate page sits next to the "EN"
      heading but opens the TARGET language's file. Placed on the target
      column here, labelled with the target language.
      // <!-- SUGGESTION: move the link under the target column (or label it
      //      with the target language) — it currently reads as "edit the
      //      English file". -->

   Surfaced but NOT silently fixed (real behaviour the operator must know):
   · saveTranslation regenerates the whole PHP file and `continue`s past
     empty scalar values, so blank targets are DROPPED from the file and fall
     back at runtime. The save confirmation counts them.
   · getLanguages()/getLanguageData() cache for 24h (`languages_list`,
     `language_data:{locale}`) and neither saveLanguage nor delete calls
     Cache::forget — new/edited/deleted languages can take up to 24h to
     appear in the pickers. Every save says so.
     // <!-- SUGGESTION: Cache::forget('languages_list') (and the per-locale
     //      key) in saveLanguage and delete. -->
   · The translate grid can only ever show keys that exist in
     storage/lang/en/<file>.php. Keys referenced by admin screens but missing
     from the EN file (backend.deleted_users, backend.cost, backend.limit_day,
     sport.bet_tax …) never appear here, so they cannot be added from this
     screen at all — only through the raw-PHP editor.
     // <!-- SUGGESTION: let the translate grid add a new key, or seed the
     //      missing keys — today "the label renders as backend.foo" has no
     //      fix reachable from this page. -->
   · Missing/unknown language id on translate/code/save endpoints answers a
     bare `die()` — a blank 200, not a 404.
     // <!-- SUGGESTION: abort(404) instead of die(). -->
   · index.blade.php pulls TinyMCE 5.0.16 from cdnjs although nothing on the
     page uses an editor, and languageForm passes an unused $max_images = 6.
     // <!-- SUGGESTION: drop both. -->
   · generateDefaultLang is routed and live, but its UI link is commented out
     (code.blade.php:37-40). It is rendered here inside the code editor,
     explicitly marked, because the endpoint exists and is reachable.

   Label policy: `backend.languages`, `backend.settings`, `backend.actions`,
   `backend.id`, `backend.insert_name`, `backend.active`, `backend.disabled`
   all resolve in the committed seed (public/default-lang/en/backend.php), so
   those labels are real. The rest of this screen is hardcoded in the blades —
   "Translate", the Backend/Frontend/Subcategories tab labels, "Translate all"
   and "Translate language" in English; the modal title ("Nuovo language"),
   the edit tooltip ("Modifica"), the validation errors ("Compila il campo
   code / locale / flag icon") and the fallback alert ("Errore sconosciuto")
   in Italian. Operator-facing English is written for the Italian ones — label
   inferred.

   UNCLEAR (recorded, not invented away):
   · No language seeder exists; production's `languages` rows are uncommitted
     data. The 13 rows below are the prototype's own observed set (carried
     over from the legacy SetLanguages). HostSkins.jsx:103 hardcodes a
     DIFFERENT 13 (adds Arabic + Română, drops Polski + Nederlands) for the
     SEO-content tabs, which the real SeoContentBlockController builds from
     this very table — the two lists should be reconciled once production
     data is known.
   · `flagicon` and `locale` values are not committed anywhere; the shapes
     used here (svg filename / underscore locale) are plausible, not verified.
   · frontend.php key names are representative — the reference records the
     file's size (~194 keys) but not its contents.
   ==================================================================== */

const { useState: hslUseState, useMemo: hslUseMemo } = React;

const hslToast = (m, detail) => window.PAYBO?.emitToast && window.PAYBO.emitToast({
  id: `hsl-${Date.now()}-${Math.floor(Math.random() * 1e6)}`,
  tx_id: m, amount: 0, currency: "HOST", player: "Languages",
  reason: detail || "Prototype state only \u2014 not persisted.",
});

/* Deterministic PRNG (mulberry32) — mock state renders identically each load. */
const hslRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
/* languages.addedTime / updateTime are bigint unix ints (migration
   2026_06_24_155202_create_languages_table). Not list columns — carried for
   shape only, and deliberately shown nowhere. */
const hslUnix = (y, m, d, h, mi) => Math.floor(new Date(y, m - 1, d, h, mi).getTime() / 1000);

/* ------------------------------------------------------------------
   `languages` rows. Columns mirror the migration; `bcw_ok` / `sa_ok` /
   `novus_ok` and the timestamps are carried because the table really has
   them, and are deliberately rendered nowhere — the real form has no field
   for any of them (see the SUGGESTION in the header).
   ------------------------------------------------------------------ */
/* ---------- the row source ------------------------------------------------
   Was thirteen invented languages with hand-written unix timestamps. Now
   `locales`, live.

   isystem's `languages` table carries novus_ok / bcw_ok / sa_ok as three
   boolean columns, so supporting a fourth integration meant a migration. Here
   that is locale_integration_support, keyed by (locale_code, integration_code),
   which is why this maps a set of codes into the three flags the screen shows
   rather than reading three columns. A fourth integration is a row, not a
   schema change. */
const hslSupports = (row, code) =>
  (row.support || []).some(x => x.integration_code === code);

const hslRow = (l) => ({
  id: l.id,
  name: l.name,
  code: l.code,
  locale: l.bcp47,
  flagicon: l.flag_icon,
  backend: l.enabled_backend ? 1 : 0,
  frontend: l.enabled_frontend ? 1 : 0,
  novus_ok: hslSupports(l, "novus") ? 1 : 0,
  bcw_ok: hslSupports(l, "bcw") ? 1 : 0,
  sa_ok: hslSupports(l, "savagetech") ? 1 : 0,
  addedTime: l.created_at ? Math.floor(Date.parse(l.created_at) / 1000) : null,
  updateTime: l.updated_at ? Math.floor(Date.parse(l.updated_at) / 1000) : null,
});

/* ------------------------------------------------------------------
   Translate-grid tabs. showTransalte (:151-162) exposes exactly these
   three; the other nine lang files are editable only through the raw-PHP
   editor (getLanguageFiles :75-100).
   ------------------------------------------------------------------ */
const HSL_TABS = [
  { key: "backend",       label: "Backend",       file: "backend.php" },
  { key: "frontend",      label: "Frontend",      file: "frontend.php" },
  { key: "subcategories", label: "Subcategories", file: "subcategories.php" },
];

/* EN source = storage/lang/en/<tab>.php. Every backend key below is a real
   key documented in the reference (it resolves in the committed seed
   public/default-lang/en/backend.php). `report_periods` illustrates an
   array-valued entry: the reference documents the nested-prefill bug
   (translate.blade.php:164-183) but does not enumerate which backend keys
   are arrays, so this group is representative of the SHAPE, not verified.
   frontend keys are representative too (see UNCLEAR in the header).
   subcategories is EMPTY on purpose: no subcategories.php seed exists
   anywhere in the repo even though config/langfiles.php:6 and this screen's
   third tab both expect one. */
const HSL_EN = {
  backend: [
    ["id", "ID"],
    ["name", "Name"],
    ["actions", "Actions"],
    ["languages", "Languages"],
    ["settings", "Settings"],
    ["deposit_methods", "Deposit methods"],
    ["insert_name", "Insert name"],
    ["active", "Active"],
    ["disabled", "Disabled"],
    ["language", "Language"],
    ["skin", "Skin"],
    ["status", "Status"],
    ["value", "Value"],
    ["print", "Print"],
    ["created_by", "Created by"],
    ["creation_date", "Creation date"],
    ["redeem_by", "Redeem by"],
    ["redeem_date", "Redeem date"],
    ["vouchers", "Vouchers"],
    ["voucher_code", "Voucher code"],
    ["voucher_pending", "Pending"],
    ["voucher_redeemed", "Redeemed"],
    ["voucher_canceled", "Canceled"],
    ["request_status_pending", "Pending"],
    ["cancelled_by_customer", "Canceled by the customer"],
    ["insert_amount", "Insert amount"],
    ["invalid_amount", "Invalid amount"],
    ["min_amount", "Minimum amount"],
    ["max_amount", "Maximum amount"],
    ["insufficient_funds", "Insufficient funds"],
    ["all_selections", "All selections"],
    ["enter_voucher_code", "Enter the voucher code"],
    ["wrong_voucher_code", "Wrong voucher code"],
    ["voucher_not_exists", "Voucher does not exist"],
    ["report_periods", { day: "Day", week: "Week", month: "Month", year: "Year" }],
  ],
  frontend: [
    ["login", "Login"],
    ["register", "Register"],
    ["logout", "Log out"],
    ["balance", "Balance"],
    ["deposit", "Deposit"],
    ["withdraw", "Withdraw"],
    ["my_account", "My account"],
    ["promotions", "Promotions"],
    ["live_casino", "Live Casino"],
    ["sports", "Sports"],
    ["search_games", "Search games"],
    ["show_more", "Show more"],
    ["forgot_password", "Forgot your password?"],
    ["contact_us", "Contact us"],
    ["terms", "Terms & Conditions"],
  ],
  subcategories: [],
};

/* Real file sizes from the reference's committed-seed table (§Language /
   i18n #3): wc -l and the approximate `=>` count per file. Shown in the code
   editor's status bar so the operator sees how much of the file the demo
   buffer represents. */
const HSL_FILES = [
  { key: "backend",       label: "Backend",       file: "backend.php",       lines: 876, keys: 857, tab: true },
  { key: "frontend",      label: "Frontend",      file: "frontend.php",      lines: 198, keys: 194, tab: true },
  { key: "subcategories", label: "Subcategories", file: "subcategories.php", lines: 0,   keys: 0,   tab: true, noSeed: true },
  { key: "validation",    label: "Validation",    file: "validation.php",    lines: 171, keys: 117 },
  { key: "sport",         label: "Sport",         file: "sport.php",         lines: 65,  keys: 60 },
  { key: "email",         label: "Email",         file: "email.php",         lines: 29,  keys: 25 },
  { key: "passwords",     label: "Passwords",     file: "passwords.php",     lines: 22,  keys: 5 },
  { key: "pagination",    label: "Pagination",    file: "pagination.php",    lines: 19,  keys: 2 },
  { key: "commissions",   label: "Commissions",   file: "commissions.php",   lines: 14,  keys: 9 },
  { key: "auth",          label: "Auth",          file: "auth.php",          lines: 9,   keys: 5 },
  { key: "financial",     label: "Financial",     file: "financial.php",     lines: 7,   keys: 2 },
  { key: "bonus",         label: "Bonus",         file: "bonus.php",         lines: 4,   keys: 1 },
];

/* Framework catalogues reproduced verbatim (auth / passwords / pagination are
   Laravel 8 defaults, so these ARE the real strings); validation is a real
   excerpt kept short — it is the file that carries nested arrays. The rest
   are representative of shape and marked as such in the editor status bar. */
const HSL_STATIC_SRC = {
  auth: [
    ["failed", "These credentials do not match our records."],
    ["password", "The provided password is incorrect."],
    ["throttle", "Too many login attempts. Please try again in :seconds seconds."],
  ],
  passwords: [
    ["reset", "Your password has been reset!"],
    ["sent", "We have emailed your password reset link!"],
    ["throttled", "Please wait before retrying."],
    ["token", "This password reset token is invalid."],
    ["user", "We can't find a user with that email address."],
  ],
  pagination: [
    ["previous", "&laquo; Previous"],
    ["next", "Next &raquo;"],
  ],
  validation: [
    ["accepted", "The :attribute must be accepted."],
    ["email", "The :attribute must be a valid email address."],
    ["required", "The :attribute field is required."],
    ["unique", "The :attribute has already been taken."],
    ["size", { numeric: "The :attribute must be :size.", file: "The :attribute must be :size kilobytes.", string: "The :attribute must be :size characters.", array: "The :attribute must contain :size items." }],
  ],
  sport: [
    ["bet_tax", "Bet tax"],
    ["coupon", "Coupon"],
    ["stake", "Stake"],
    ["odds", "Odds"],
    ["potential_win", "Potential win"],
    ["cashout", "Cashout"],
  ],
  email: [
    ["subject_welcome", "Welcome to :skin"],
    ["subject_reset", "Reset your password"],
    ["greeting", "Hello :name,"],
    ["regards", "Regards,"],
  ],
  commissions: [
    ["profile", "Commission profile"],
    ["period", "Period"],
    ["payout", "Payout"],
  ],
  financial: [
    ["transfer_in", "Transfer in"],
    ["transfer_out", "Transfer out"],
  ],
  bonus: [
    ["bonus_expired", "Your bonus has expired."],
  ],
};

/* ------------------------------------------------------------------
   Two separate pools, on purpose:
   · HSL_MT  — what the Google Translate endpoint would hand back. Keys with
     no entry model the documented ok:false path (quota / dead key / bad
     pair): translateByGoogle logs to the `google_translate` channel and the
     client leaves the field UNTOUCHED rather than clobbering it with
     English. This demo build has no API key wired, so every miss takes that
     branch honestly instead of faking a translation.
   · coverage — how much of the pool is already saved in
     storage/lang/{code}/backend.php. The rest starts empty (outlined red,
     exactly like translate.blade.php).
   ------------------------------------------------------------------ */
const HSL_MT = {
  it: {
    id: "ID", name: "Nome", actions: "Azioni", languages: "Lingue", settings: "Impostazioni",
    deposit_methods: "Metodi di deposito", insert_name: "Inserisci il nome", active: "Attivo",
    disabled: "Disattivato", language: "Lingua", skin: "Skin", status: "Stato", value: "Valore",
    print: "Stampa", created_by: "Creato da", creation_date: "Data di creazione",
    redeem_by: "Riscattato da", redeem_date: "Data di riscatto", vouchers: "Voucher",
    voucher_code: "Codice voucher", voucher_pending: "In attesa", voucher_redeemed: "Riscattato",
    voucher_canceled: "Annullato", request_status_pending: "In attesa",
    cancelled_by_customer: "Annullato dal cliente", insert_amount: "Inserisci l'importo",
    invalid_amount: "Importo non valido", min_amount: "Importo minimo", max_amount: "Importo massimo",
    insufficient_funds: "Fondi insufficienti", all_selections: "Tutte le selezioni",
    enter_voucher_code: "Inserisci il codice voucher", wrong_voucher_code: "Codice voucher errato",
    voucher_not_exists: "Il voucher non esiste",
    "report_periods.day": "Giorno", "report_periods.week": "Settimana",
    "report_periods.month": "Mese", "report_periods.year": "Anno",
    login: "Accedi", register: "Registrati", logout: "Esci", balance: "Saldo", deposit: "Deposita",
    withdraw: "Preleva", my_account: "Il mio account", promotions: "Promozioni",
    live_casino: "Casinò Live", sports: "Sport", search_games: "Cerca giochi",
    show_more: "Mostra altro", forgot_password: "Password dimenticata?", contact_us: "Contattaci",
    terms: "Termini e condizioni",
  },
  es: {
    id: "ID", name: "Nombre", actions: "Acciones", languages: "Idiomas", settings: "Configuración",
    deposit_methods: "Métodos de depósito", insert_name: "Introduce el nombre", active: "Activo",
    disabled: "Desactivado", language: "Idioma", skin: "Skin", status: "Estado", value: "Valor",
    print: "Imprimir", created_by: "Creado por", creation_date: "Fecha de creación",
    redeem_by: "Canjeado por", redeem_date: "Fecha de canje", vouchers: "Vouchers",
    voucher_code: "Código de voucher", voucher_pending: "Pendiente", voucher_redeemed: "Canjeado",
    voucher_canceled: "Cancelado", request_status_pending: "Pendiente",
    cancelled_by_customer: "Cancelado por el cliente", insert_amount: "Introduce el importe",
    invalid_amount: "Importe no válido", min_amount: "Importe mínimo", max_amount: "Importe máximo",
    insufficient_funds: "Fondos insuficientes", all_selections: "Todas las selecciones",
    enter_voucher_code: "Introduce el código de voucher", wrong_voucher_code: "Código de voucher incorrecto",
    voucher_not_exists: "El voucher no existe",
    "report_periods.day": "Día", "report_periods.week": "Semana",
    "report_periods.month": "Mes", "report_periods.year": "Año",
    login: "Iniciar sesión", register: "Registrarse", logout: "Cerrar sesión", balance: "Saldo",
    deposit: "Depositar", withdraw: "Retirar", my_account: "Mi cuenta", promotions: "Promociones",
    live_casino: "Casino en vivo", sports: "Deportes", search_games: "Buscar juegos",
    show_more: "Ver más", forgot_password: "¿Olvidaste tu contraseña?", contact_us: "Contáctanos",
    terms: "Términos y condiciones",
  },
  pt: {
    id: "ID", name: "Nome", actions: "Ações", languages: "Idiomas", settings: "Definições",
    active: "Ativo", disabled: "Desativado", status: "Estado", value: "Valor", print: "Imprimir",
    skin: "Skin", language: "Idioma", vouchers: "Vouchers", voucher_code: "Código de voucher",
    voucher_pending: "Pendente", voucher_redeemed: "Resgatado", voucher_canceled: "Cancelado",
    login: "Entrar", register: "Registar", logout: "Sair", balance: "Saldo", deposit: "Depositar",
    withdraw: "Levantar", promotions: "Promoções", sports: "Desporto",
  },
  de: {
    id: "ID", name: "Name", actions: "Aktionen", languages: "Sprachen", settings: "Einstellungen",
    active: "Aktiv", disabled: "Deaktiviert", status: "Status", value: "Wert", print: "Drucken",
    skin: "Skin", language: "Sprache", vouchers: "Gutscheine", voucher_code: "Gutscheincode",
    login: "Anmelden", register: "Registrieren", logout: "Abmelden", balance: "Guthaben",
    deposit: "Einzahlen", withdraw: "Auszahlen", promotions: "Aktionen", sports: "Sport",
  },
  fr: {
    id: "ID", name: "Nom", actions: "Actions", languages: "Langues", settings: "Paramètres",
    active: "Actif", disabled: "Désactivé", status: "Statut", value: "Valeur", print: "Imprimer",
    skin: "Skin", language: "Langue", vouchers: "Bons", voucher_code: "Code du bon",
    login: "Connexion", register: "S'inscrire", logout: "Déconnexion", balance: "Solde",
    deposit: "Dépôt", withdraw: "Retrait", promotions: "Promotions", sports: "Sports",
  },
  tr: {
    id: "ID", name: "Ad", actions: "İşlemler", languages: "Diller", settings: "Ayarlar",
    active: "Aktif", disabled: "Devre dışı", status: "Durum", value: "Değer", print: "Yazdır",
    skin: "Skin", language: "Dil", login: "Giriş", register: "Kayıt ol", logout: "Çıkış",
    balance: "Bakiye", deposit: "Para yatır", withdraw: "Para çek", sports: "Spor",
  },
  pl: {
    id: "ID", name: "Nazwa", actions: "Akcje", languages: "Języki", settings: "Ustawienia",
    active: "Aktywny", disabled: "Wyłączony", status: "Status", value: "Wartość", print: "Drukuj",
    login: "Zaloguj się", register: "Zarejestruj się", balance: "Saldo", sports: "Sport",
  },
  nl: {
    name: "Naam", actions: "Acties", languages: "Talen", settings: "Instellingen",
    active: "Actief", disabled: "Uitgeschakeld", status: "Status", value: "Waarde", print: "Afdrukken",
    login: "Inloggen", register: "Registreren", balance: "Saldo", sports: "Sport",
  },
  zh: {
    name: "名称", actions: "操作", languages: "语言", settings: "设置",
    active: "启用", disabled: "禁用", status: "状态", value: "数值", print: "打印",
    login: "登录", register: "注册", balance: "余额", sports: "体育",
  },
  hu: {
    name: "Név", actions: "Műveletek", languages: "Nyelvek", settings: "Beállítások",
    active: "Aktív", disabled: "Letiltva", status: "Állapot", value: "Érték", print: "Nyomtatás",
    login: "Belépés", register: "Regisztráció", balance: "Egyenleg", sports: "Sport",
  },
};
/* pt_br and br are Portuguese variants — same pool. `br` duplicates pt_br in
   the languages table (see UNCLEAR); it is left with zero coverage so the
   "nothing translated yet" path is reachable. */
HSL_MT.pt_br = HSL_MT.pt;
HSL_MT.br = HSL_MT.pt;

/* How much of the pool is already on disk, per language code. */
const HSL_COVERAGE = { it: 1, es: 1, pt: 0.7, de: 0.6, fr: 0.5, tr: 0.45, pl: 0.35, nl: 0.3, zh: 0.25, hu: 0.2, pt_br: 0.55, br: 0 };

/* Flatten an EN tab into rows: { key, path, label, src, parent } — `path` is
   the POST name saveTranslation receives (translations[key] or
   translations[parent][subkey]); `label` is the key as altnome() normalizes
   it for the textarea id (utils.php:115). */
const hslFlatten = (tab) => {
  const out = [];
  (HSL_EN[tab] || []).forEach(([key, val]) => {
    if (val && typeof val === "object") {
      out.push({ key, path: key, src: null, group: true });
      Object.keys(val).forEach(sub => out.push({ key: sub, path: `${key}.${sub}`, src: val[sub], parent: key }));
    } else {
      out.push({ key, path: key, src: val });
    }
  });
  return out;
};

/* What storage/lang/{code}/<tab>.php already holds. English is its own
   source, so the target column equals the EN column there. */
const hslDiskFor = (lang, tab) => {
  const rows = hslFlatten(tab).filter(r => !r.group);
  const out = {};
  if (lang.code === "en") { rows.forEach(r => { out[r.path] = r.src; }); return out; }
  const pool = HSL_MT[lang.code] || {};
  const cov = HSL_COVERAGE[lang.code] || 0;
  const rng = hslRng(0x1a5 + lang.id * 977);
  rows.forEach(r => {
    const mt = pool[r.path];
    out[r.path] = (mt && rng() < cov) ? mt : "";
  });
  return out;
};

/* PHP buffer for the code editor. saveCode writes the buffer VERBATIM, so
   this is a source view, not a form. */
const hslPhpEscape = (s) => String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
const hslPhpSource = (pairs) => {
  if (!pairs || !pairs.length) return "";
  const body = pairs.map(([k, v]) => {
    if (v && typeof v === "object") {
      const inner = Object.keys(v).map(sk => `        '${sk}' => '${hslPhpEscape(v[sk])}',`).join("\n");
      return `    '${k}' => [\n${inner}\n    ],`;
    }
    return `    '${k}' => '${hslPhpEscape(v)}',`;
  }).join("\n");
  return `<?php\n\nreturn [\n${body}\n];\n`;
};

/* The buffer a given (language, file) pair would load. Non-EN languages only
   ever have the two files this screen's translate grid can write, plus
   whatever was pasted into the editor; everything else does not exist on
   disk yet — getLanguageFiles simply returns nothing and saveCode creates
   the file on first save. */
const hslBufferFor = (lang, fileKey) => {
  if (fileKey === "subcategories") return "";
  if (fileKey === "backend" || fileKey === "frontend") {
    const disk = hslDiskFor(lang, fileKey);
    const pairs = (HSL_EN[fileKey] || []).map(([k, v]) => {
      if (v && typeof v === "object") {
        const sub = {};
        Object.keys(v).forEach(sk => { const t = disk[`${k}.${sk}`]; if (t) sub[sk] = t; });
        return Object.keys(sub).length ? [k, sub] : null;
      }
      const t = disk[k];
      return t ? [k, t] : null;               // empty values are dropped by saveTranslation
    }).filter(Boolean);
    return hslPhpSource(pairs);
  }
  if (lang.code !== "en") return "";
  return hslPhpSource(HSL_STATIC_SRC[fileKey] || []);
};

const hslLineCount = (s) => (s ? s.split("\n").length : 0);
/* Rows the save endpoint would drop (`continue` on empty scalars). */
const hslBlankCount = (vals) => Object.keys(vals).filter(k => !String(vals[k] || "").trim()).length;
/* A typed " corrupts the generated PHP file today (see divergence 5). */
const hslHasQuote = (v) => /["\\]/.test(String(v || ""));

/* ==================================================================
   Modal shell — the real screen uses the generic
   generaModalGestione() → admin/utils/modal.blade.php wrapper.
   ================================================================== */
const HslModal = ({ title, sub, onClose, children, footer }) => (
  <div className="bp-modal-scrim hsl-scrim" onClick={onClose}>
    <div className="bp-modal hsl-modal" onClick={e => e.stopPropagation()}>
      <div className="hsl-modal__head">
        <div>
          <div className="hsl-modal__title">{title}</div>
          {sub && <div className="hsl-modal__sub">{sub}</div>}
        </div>
        <button className="btn btn--ghost btn--icon btn--sm" onClick={onClose} title="Close"><Icon name="x" size={14} /></button>
      </div>
      <div className="hsl-modal__body">{children}</div>
      {footer && <div className="hsl-modal__foot">{footer}</div>}
    </div>
  </div>
);

/* ==================================================================
   Create / edit form — GET /languages/form/ then POST /languages/saveLanguage/
   Fields exactly as forms/language.blade.php: name, code, locale_code
   (→ languages.locale), flagicon, backend, frontend. Inline validation
   only — saveLanguage :684-714 has no uniqueness, format or length rule.
   ================================================================== */
const HslFormModal = ({ row, onClose, onSave }) => {
  const isNew = !row;
  const [d, setD] = hslUseState(() => ({
    name: row ? row.name : "",
    code: row ? row.code : "",
    /* forms/language.blade.php:48 prefills this input with $row->locale.
       Evident intent implemented — the row's real flagicon (divergence 1). */
    flagicon: row ? row.flagicon : "",
    locale: row ? row.locale : "",
    backend: row ? !!row.backend : true,
    frontend: row ? !!row.frontend : true,
  }));
  const [err, setErr] = hslUseState({});
  const set = (k, v) => { setD(x => ({ ...x, [k]: v })); setErr(e => ({ ...e, [k]: null })); };

  const submit = () => {
    /* Real messages: backend.insert_name ("Insert name") for name, then the
       hardcoded Italian "Compila il campo code / locale / flag icon".
       English written here — label inferred. */
    const e = {};
    if (!d.name.trim()) e.name = "Insert name";
    if (!d.code.trim()) e.code = "Fill in the code field";
    if (!d.locale.trim()) e.locale = "Fill in the locale field";
    if (!d.flagicon.trim()) e.flagicon = "Fill in the flag icon field";
    setErr(e);
    if (Object.keys(e).length) return;
    onSave({ ...d, name: d.name.trim(), code: d.code.trim(), locale: d.locale.trim(), flagicon: d.flagicon.trim() });
  };

  const field = (k, label, placeholder, tip) => (
    <div className="hsl-field">
      <label className="form-label">* {label}{tip && <Tip size={12}>{tip}</Tip>}</label>
      <input className={`input input--sm${err[k] ? " hsl-input--err" : ""}`} value={d[k]} placeholder={placeholder}
        onChange={e => set(k, e.target.value)} />
      {err[k] && <div className="hsl-err">{err[k]}</div>}
    </div>
  );

  return (
    <HslModal
      /* Real modal title is the hardcoded Italian "Nuovo language" (and
         "Modifica" on the edit tooltip) — English written here. */
      title={isNew ? "New language" : `Edit ${row.name}`}
      sub={isNew ? "POST /languages/saveLanguage/ — creates a row in `languages`" : `POST /languages/saveLanguage/?id=${row.id}`}
      onClose={onClose}
      footer={<>
        <button className="btn btn--secondary btn--sm" onClick={onClose}>Close</button>
        <button className="btn btn--primary btn--sm" onClick={submit}><Icon name="check" size={13} /> Save</button>
      </>}
    >
      <div className="hsl-form">
        {field("name", "Name", "English", "languages.name — shown in the list and used as the Translate link label.")}
        {field("code", "Code", "en", <>Written to <code>languages.code</code>. This is the folder name under <code>storage/lang/</code> that the translate grid and the raw-PHP editor write into, and the value <code>POST /setLanguage/&#123;code&#125;</code> stores in the <code>applocale</code> session key.</>)}
        {field("locale", "Locale code", "en_US", <>Posted as <code>locale_code</code>, stored to <code>languages.locale</code> (varchar 10). Used as the cache key suffix in <code>getLanguageData()</code> (<code>language_data:&#123;locale&#125;</code>).</>)}
        {field("flagicon", "Flag icon", "united-kingdom.svg", <>Rendered by the admin topbar language picker. On the real edit form this input is prefilled with the <em>locale</em> instead of the flag icon — fixed here, see the file header.</>)}
        <div className="hsl-field hsl-field--switch">
          <label className="form-label">Active on backend<Tip size={12}>The only one of the two flags with a committed consumer: <code>UsersController::setLanguage</code> refuses any code whose row does not have <code>backend = 1</code>, so an unchecked language disappears from the admin picker.</Tip></label>
          <Toggle value={d.backend} onChange={v => set("backend", v)} onLabel="Active" offLabel="Disabled" size="sm" />
        </div>
        <div className="hsl-field hsl-field--switch">
          <label className="form-label">Active on frontend<Tip size={12}>Editable here, but no committed code filters on it — the player-facing header lists every row from <code>getLanguages()</code> unfiltered. Whether anything enforces it is UNCLEAR in the reference.</Tip></label>
          <Toggle value={d.frontend} onChange={v => set("frontend", v)} onLabel="Active" offLabel="Disabled" size="sm" />
        </div>
      </div>
      <div className="hsl-note">
        Saving does not bust the 24h <code>languages_list</code> / <code>language_data:&#123;locale&#125;</code> caches, so a new or
        renamed language can take up to a day to show up in the pickers.
      </div>
    </HslModal>
  );
};

/* Delete — the real row action is a plain GET behind the shared
   deleteConfirm() JS confirm, and it is the ONLY endpoint on this screen
   that re-checks isadmin() server-side. */
const HslDeleteModal = ({ row, onClose, onConfirm }) => (
  <HslModal
    title="Delete language"
    sub={`GET /languages/delete/${row.id}/`}
    onClose={onClose}
    footer={<>
      <button className="btn btn--secondary btn--sm" onClick={onClose}>Cancel</button>
      <button className="btn btn--danger btn--sm" onClick={onConfirm}><Icon name="trash" size={13} /> Delete</button>
    </>}
  >
    <p className="hsl-p">Delete <b>{row.name}</b> (<code>{row.code}</code>) from the <code>languages</code> table?</p>
    <div className="hsl-note hsl-note--warn">
      The row goes away, but <code>storage/lang/{row.code}/</code> stays on disk untouched, and the 24h language caches are not
      flushed — the language can keep appearing in pickers for up to a day. This is also the only action on this screen the
      server re-checks <code>isadmin()</code> for.
    </div>
  </HslModal>
);

/* ==================================================================
   Translate grid — GET /languages/translate/{id}?tab=backend|frontend|subcategories
   Left: EN source (read-only). Right: target textareas, empty ones outlined
   red. Per-row Translate (hidden when the target IS English), Translate all
   in chunks of 50, and a Save that posts the whole key set.
   ================================================================== */
const HslTranslate = ({ lang, tab, onTab, onBack, onCode }) => {
  const rows = hslUseMemo(() => hslFlatten(tab), [tab]);
  const [vals, setVals] = hslUseState(() => hslDiskFor(lang, tab));
  const [dirty, setDirty] = hslUseState(false);

  /* Re-seed when the tab or language changes (the real page is a full reload). */
  const [seen, setSeen] = hslUseState(`${lang.id}:${tab}`);
  if (seen !== `${lang.id}:${tab}`) { setSeen(`${lang.id}:${tab}`); setVals(hslDiskFor(lang, tab)); setDirty(false); }

  const isEn = lang.code === "en";
  const scalars = rows.filter(r => !r.group);
  const filled = scalars.filter(r => String(vals[r.path] || "").trim()).length;
  const missing = scalars.length - filled;
  const quoted = scalars.filter(r => hslHasQuote(vals[r.path])).length;
  const pool = HSL_MT[lang.code] || {};

  const set = (path, v) => { setVals(x => ({ ...x, [path]: v })); setDirty(true); };

  /* GET /translateByGoogle — services.google_translate.key. A miss models the
     documented ok:false branch: the field is left untouched, never clobbered
     with the English source. cleanTranslation() also repairs %s/%d
     placeholders Google mangles, which is why they survive here. */
  const translateOne = (r) => {
    const mt = pool[r.path];
    if (!mt) {
      hslToast("Translation failed — field left untouched", `translateByGoogle answered ok:false for "${r.path}" (no API key wired in this build; on the platform this is quota, a dead key or an unsupported pair). The error goes to the google_translate log channel and the client deliberately leaves your text alone instead of overwriting it with English.`);
      return;
    }
    set(r.path, mt);
  };

  const translateAll = () => {
    const targets = scalars.filter(r => !String(vals[r.path] || "").trim());
    if (!targets.length) { hslToast("Nothing to translate", "Every key on this tab already has a value."); return; }
    const next = { ...vals };
    let ok = 0;
    targets.forEach(r => { if (pool[r.path]) { next[r.path] = pool[r.path]; ok++; } });
    setVals(next); setDirty(true);
    const chunks = Math.ceil(scalars.length / 50);   // ×50 per POST — stays under Google's 128-segment limit
    hslToast(`Translate all — ${ok} of ${targets.length} filled`,
      `POST /translateByGoogleBatch in ${chunks} chunk${chunks === 1 ? "" : "s"} of 50 keys. ${targets.length - ok} key${targets.length - ok === 1 ? "" : "s"} came back ok:false and were left untouched (see the google_translate log channel).`);
  };

  const save = () => {
    const blanks = hslBlankCount(Object.fromEntries(scalars.map(r => [r.path, vals[r.path]])));
    setDirty(false);
    hslToast(`Saved ${HSL_TABS.find(t => t.key === tab).file} for ${lang.name}`,
      `POST /languages/saveTranslation/${lang.id} regenerates storage/lang/${lang.code}/${tab}.php from scratch. ${blanks} empty key${blanks === 1 ? " was" : "s were"} skipped and dropped from the file — those labels fall back at runtime.${quoted ? ` ${quoted} value${quoted === 1 ? " contains" : "s contain"} a quote or backslash; escaped on write (the live endpoint does not — see the file header).` : ""} Translations are live immediately: the runtime lang path is storage/lang/.`);
  };

  return (
    <>
      <div className="hsl-sub">
        <button className="btn btn--ghost btn--sm" onClick={onBack}><Icon name="chevron_left" size={13} /> All languages</button>
        <span className="hsl-crumb">
          <b>{lang.name}</b>
          <code>{lang.code}</code>
          <span className={`chip ${lang.backend ? "chip--ok" : "chip--neutral"}`}>{lang.backend ? "Backend active" : "Backend disabled"}</span>
          <span className={`chip ${lang.frontend ? "chip--ok" : "chip--neutral"}`}>{lang.frontend ? "Frontend active" : "Frontend disabled"}</span>
        </span>
      </div>

      <div className="hsl-tabs" role="tablist">
        {HSL_TABS.map(t => (
          <button key={t.key} className={`hsl-tab${t.key === tab ? " hsl-tab--on" : ""}`} onClick={() => onTab(t.key)}>
            {t.label}
            <span className="hsl-tabfile">{t.file}</span>
          </button>
        ))}
      </div>

      {tab === "subcategories" && rows.length === 0 ? (
        <div className="panel hsl-empty">
          <Icon name="alert" size={20} />
          <div className="hsl-empty__t">No English source to translate</div>
          <div className="hsl-empty__b">
            The grid iterates <code>storage/lang/en/subcategories.php</code>, and no <code>subcategories.php</code> seed exists
            anywhere in the platform — <code>public/default-lang/en/</code> ships the other eleven files but not this one, even
            though <code>config/langfiles.php</code> and this tab both expect it. Until someone writes the English file (only the
            raw-PHP editor can), this tab has nothing to show for any language.
          </div>
        </div>
      ) : (
        <>
          <div className="hsl-bar">
            <div className="hsl-bar__stats">
              <span className="hsl-stat"><b>{scalars.length}</b> keys</span>
              <span className="hsl-stat hsl-stat--ok"><b>{filled}</b> translated</span>
              <span className={`hsl-stat${missing ? " hsl-stat--miss" : ""}`}><b>{missing}</b> empty</span>
              {quoted > 0 && <span className="hsl-stat hsl-stat--warn"><b>{quoted}</b> with quotes</span>}
              {dirty && <span className="hsl-stat hsl-stat--dirty">unsaved changes</span>}
            </div>
            <div className="hsl-bar__acts">
              {!isEn && <button className="btn btn--secondary btn--sm" onClick={translateAll}><Icon name="zap" size={13} /> Translate all</button>}
              <button className="btn btn--primary btn--sm" onClick={save}><Icon name="check" size={13} /> Save</button>
            </div>
          </div>

          <div className="hsl-note">
            Save posts <b>every</b> key on this tab and regenerates the file from scratch — empty values are skipped, so clearing a
            box deletes that key and the label falls back to the raw <code>{tab}.&#123;key&#125;</code> string at runtime. Keys that are
            missing from the English file never appear here at all and can only be added through the raw-PHP editor.
            {isEn && <> This <b>is</b> the English file: the two columns read from the same source, and the per-row Translate links are hidden (the real page hides them whenever the target code is <code>en</code>).</>}
          </div>

          <div className="panel hsl-grid">
            <div className="hsl-grid__head">
              <div className="hsl-h">Key</div>
              <div className="hsl-h">English source <span className="hsl-h__f">storage/lang/en/{tab}.php</span></div>
              <div className="hsl-h">
                {lang.name} <span className="hsl-h__f">storage/lang/{lang.code}/{tab}.php</span>
                {/* Real placement is next to the EN heading although it opens
                    the TARGET file (divergence 7). */}
                <button className="hsl-link" onClick={() => onCode(tab)}>
                  <Icon name="rules" size={11} /> Edit php
                </button>
              </div>
            </div>

            {rows.map(r => r.group ? (
              <div key={`g-${r.key}`} className="hsl-group">
                <Icon name="list" size={12} /> <code>{r.key}</code>
                <span className="hsl-group__n">array — sub-keys post as <code>translations[{r.key}][…]</code></span>
              </div>
            ) : (
              <div key={r.path} className={`hsl-row${r.parent ? " hsl-row--sub" : ""}`}>
                <div className="hsl-cell hsl-cell--key">
                  <code>{r.parent ? r.key : r.path}</code>
                  {!String(vals[r.path] || "").trim() && <span className="hsl-dot" title="Not translated — this key will be dropped from the file on save" />}
                </div>
                <div className="hsl-cell">
                  {/* originale_<key>, normalized by altnome() (utils.php:115) */}
                  <textarea className="hsl-ta hsl-ta--src" value={r.src || ""} readOnly rows={1} spellCheck={false} />
                </div>
                <div className="hsl-cell hsl-cell--tgt">
                  <textarea
                    className={`hsl-ta${String(vals[r.path] || "").trim() ? "" : " hsl-ta--empty"}${hslHasQuote(vals[r.path]) ? " hsl-ta--warn" : ""}`}
                    value={vals[r.path] || ""} rows={1} spellCheck={false}
                    onChange={e => set(r.path, e.target.value)} />
                  {hslHasQuote(vals[r.path]) && (
                    <div className="hsl-inlinewarn">
                      <Icon name="alert" size={11} /> Quote / backslash — escaped on save here; the live endpoint writes it raw and breaks the generated PHP file.
                    </div>
                  )}
                  {!isEn && (
                    <button className="hsl-link hsl-link--row" onClick={() => translateOne(r)} title="GET /translateByGoogle">
                      <Icon name="zap" size={11} /> Translate
                    </button>
                  )}
                </div>
              </div>
            ))}
          </div>
        </>
      )}
    </>
  );
};

/* ==================================================================
   Raw PHP editor — GET /languages/code/{id}?tab=… → POST /languages/saveCode/{id}
   Twelve file tabs; the buffer is written to disk verbatim as PHP source.
   ================================================================== */
const HslCode = ({ lang, file, onFile, onBack, onTranslate }) => {
  const meta = HSL_FILES.find(f => f.key === file) || HSL_FILES[0];
  const [buf, setBuf] = hslUseState(() => hslBufferFor(lang, file));
  const [seen, setSeen] = hslUseState(`${lang.id}:${file}`);
  if (seen !== `${lang.id}:${file}`) { setSeen(`${lang.id}:${file}`); setBuf(hslBufferFor(lang, file)); }

  const lines = hslLineCount(buf);
  const exists = buf !== "";

  const save = () => hslToast(`Saved storage/lang/${lang.code}/${meta.file}`,
    `POST /languages/saveCode/${lang.id} writes the editor buffer verbatim as PHP. No syntax check runs, and because the runtime lang path is storage/lang/, a broken file takes down every screen that reads ${meta.key}.* on the next request.`);

  const seed = () => hslToast("Regenerated storage/lang/en/ from the seed",
    "GET /languages/generateDefaultLang/ copies every file in public/default-lang/en/ over storage/lang/en/. It overwrites the live English catalogue — including keys operators added by hand — and it does not create subcategories.php, which the seed folder does not contain.");

  return (
    <>
      <div className="hsl-sub">
        <button className="btn btn--ghost btn--sm" onClick={onBack}><Icon name="chevron_left" size={13} /> All languages</button>
        <span className="hsl-crumb">
          <b>{lang.name}</b><code>{lang.code}</code>
          <span className="hsl-crumb__path">storage/lang/{lang.code}/{meta.file}</span>
        </span>
        {meta.tab && meta.key !== "subcategories" && (
          <button className="btn btn--ghost btn--sm" onClick={() => onTranslate(meta.key)}><Icon name="list" size={13} /> Open translate grid</button>
        )}
      </div>

      <div className="hsl-note hsl-note--danger">
        <Icon name="shield" size={13} />
        <span>
          This endpoint writes <b>arbitrary PHP</b> to the server and is gated by login + 2FA only — no role check. The sidebar
          hides the whole screen from everyone but the super admin, but <code>POST /languages/saveCode/&#123;id&#125;</code> itself
          accepts any authenticated backoffice user of any level. Only three of these twelve files (Backend, Frontend,
          Subcategories) have a safe form editor; the other nine can be changed <i>only</i> from here.
        </span>
      </div>

      <div className="hsl-tabs hsl-tabs--files" role="tablist">
        {HSL_FILES.map(f => (
          <button key={f.key} className={`hsl-tab${f.key === file ? " hsl-tab--on" : ""}`} onClick={() => onFile(f.key)}>
            {f.label}
            {f.tab && <span className="hsl-tabmark" title="Also editable from the translate grid">form</span>}
          </button>
        ))}
      </div>

      <div className="panel hsl-editor">
        <div className="hsl-editor__head">
          <span className="hsl-editor__file"><Icon name="rules" size={12} /> {meta.file}</span>
          <span className="hsl-editor__meta">
            {exists
              ? <>{lines} line{lines === 1 ? "" : "s"} in the buffer{meta.lines ? <> · seed file is {meta.lines} lines / ≈{meta.keys} keys</> : null}</>
              : <>file does not exist yet — saving creates it</>}
          </span>
        </div>
        <div className="hsl-editor__body">
          <div className="hsl-editor__gutter" aria-hidden="true">
            {Array.from({ length: Math.max(lines, 12) }, (_, i) => <span key={i}>{i + 1}</span>)}
          </div>
          <textarea className="hsl-editor__ta" spellCheck={false} value={buf} onChange={e => setBuf(e.target.value)}
            placeholder={"<?php\n\nreturn [\n    'key' => 'Value',\n];"} />
        </div>
        {!exists && (
          <div className="hsl-editor__note">
            {meta.noSeed
              ? <>No <code>{meta.file}</code> is committed anywhere — not in <code>public/default-lang/en/</code> either — even though <code>config/langfiles.php</code> expects it. Saving writes the first copy.</>
              : <>Nothing has been written to <code>storage/lang/{lang.code}/{meta.file}</code> yet, so this language falls back to the English catalogue for every <code>{meta.key}.*</code> key.</>}
          </div>
        )}
      </div>

      <div className="hsl-coderow">
        <button className="btn btn--primary btn--sm" onClick={save}><Icon name="check" size={13} /> Save file</button>
        {/* Route is live (admin.php:1356); only its link is commented out in
            code.blade.php:37-40. Rendered here, explicitly marked. */}
        <button className="btn btn--secondary btn--sm" onClick={seed}><Icon name="refresh" size={13} /> Regenerate English defaults</button>
        <span className="hsl-codehint">
          <code>GET /languages/generateDefaultLang/</code> is routed and reachable today, but its button is commented out of the
          real view — so the only committed path from repo to runtime translations is currently invisible to operators.
          It always targets <code>storage/lang/en/</code>, whichever language you have open.
        </span>
      </div>
    </>
  );
};

/* ==================================================================
   SetLanguages — list surface + the two drill-downs
   (app.jsx case "settings-languages"; this file loads after
   HostSettings.jsx so this definition wins over the legacy stub)
   ================================================================== */
const SetLanguages = () => {
  window.useLocale && window.useLocale();

  const feed = useHrsFetch(() => window.sb.list("locales", { limit: 200 }), []);
  const rows = hslUseMemo(() => (feed.data || []).map(hslRow), [feed.data]);
  const [view, setView] = hslUseState({ name: "list" });          // list | translate | code
  const [modal, setModal] = hslUseState(null);                    // {edit:row} | "new" | {del:row}

  /* Filters follow the real apply-on-Search semantics (#kt_search); Reset is
     the handler ajax.js binds to a button the blade never renders. */
  const HSL_FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "tag", placeholder: "e.g. 5", width: 140, tip: <>Exact match on <code>languages.id</code> — not a range and not a LIKE.</> },
    { key: "name", label: "Name", type: "text", icon: "search", placeholder: "Search name…", grow: true, tip: <><code>LIKE '%…%'</code> on <code>languages.name</code>.</> },
  ];
  const [draft, setDraft] = hslUseState({ id: "", name: "" });
  const [applied, setApplied] = hslUseState({ id: "", name: "" });
  const [sort, setSort] = hslUseState({ key: "id", dir: "desc" }); // ajax.js L20 — initial order id DESC
  const [page, setPage] = hslUseState(0);
  const [pageSize, setPageSize] = hslUseState(50);                 // 50 default, lengthMenu 5/10/25/50

  const filtered = hslUseMemo(() => rows.filter(r => {
    if (applied.id && String(r.id) !== String(applied.id).trim()) return false;
    if (applied.name && !r.name.toLowerCase().includes(applied.name.trim().toLowerCase())) return false;
    return true;
  }), [rows, applied]);

  const sorted = hslUseMemo(() => {
    const dir = sort.dir === "asc" ? 1 : -1;
    /* `translate` sorts by name — the header is clickable on the real screen
       but the order switch only knows id/name (divergence 3). */
    const k = sort.key === "translate" ? "name" : sort.key;
    return filtered.slice().sort((a, b) => (k === "id" ? a.id - b.id : String(a[k]).localeCompare(String(b[k]))) * dir);
  }, [filtered, sort]);

  const paged = sorted.slice(page * pageSize, page * pageSize + pageSize);

  const openTranslate = (row, tab) => setView({ name: "translate", id: row.id, tab: tab || "backend" });
  const openCode = (row, file) => setView({ name: "code", id: row.id, file: file || "backend" });

  /* Create, edit and delete are writes, and src/supabase.js is read-only by
     design (stage 7 of docs/WORK_PLAN.md). These used to mutate a local array
     and toast "Saved" / "Created" / "Deleted" — three success messages for
     three changes that survive nothing. They now report what they WOULD do. */
  const doSave = (d) => {
    const editing = modal && modal.edit;
    hslToast("Not saved — no write path yet",
      editing
        ? `Would update locales id ${modal.edit.id}: name "${d.name}", code "${d.code}", bcp47 "${d.locale}", enabled_backend ${!!d.backend}, enabled_frontend ${!!d.frontend}.`
        : `Would insert into locales: name "${d.name}", code "${d.code}", bcp47 "${d.locale}". Integration support is a row per integration in locale_integration_support, not a column.`);
    setModal(null);
  };

  const doDelete = (row) => {
    setModal(null);
    hslToast("Not deleted — no write path yet",
      `Would set locales.deleted_at on id ${row.id} (${row.name}). Soft delete, not a row removal — isystem hard-deletes and leaves storage/lang/${row.code}/ on disk.`);
  };

  const current = view.id != null ? rows.find(r => r.id === view.id) : null;
  /* Missing/unknown id answers a bare die() — a blank 200 page, not a 404. */
  if (view.name !== "list" && !current) {
    return (
      <HrsShell title="Languages" gate={<>Sidebar entry renders for <code>isadmin()</code> (super admin) only.</>}>
        <div className="panel hsl-empty">
          <Icon name="alert" size={20} />
          <div className="hsl-empty__t">No such language</div>
          <div className="hsl-empty__b">The real translate / code endpoints answer an unknown id with a bare <code>die()</code> — a blank 200 response, not a 404.</div>
          <button className="btn btn--secondary btn--sm" onClick={() => setView({ name: "list" })}>Back to the list</button>
        </div>
      </HrsShell>
    );
  }

  const GATE = (
    <>
      The sidebar entry renders for <code>isadmin()</code> (SUPER_ADMIN, <code>user_level 0</code>) only, inside a Settings ▾
      dropdown gated <code>isadmin() || isSkinAdmin() || $enable_agents_operators</code>.
    </>
  );
  const GATE_NOTE = (
    <> Server side, honestly: only <code>delete()</code> re-checks <code>isadmin()</code>. <code>saveLanguage</code>,
      <code>showTransalte</code>, <code>saveTranslation</code>, <code>showCode</code>, <code>saveCode</code> and
      <code>generateDefaultLang</code> carry <b>no</b> role check at all — any authenticated 2FA'd backoffice user of any level
      can reach them by URL, and <code>saveCode</code> writes arbitrary PHP to disk. This demo session is a super admin.</>
  );

  if (view.name === "translate") {
    return (
      <HrsShell title={`Translate — ${current.name}`}
        subtitle={<>Two-column key grid over <code>storage/lang/</code>. Saved values change live backoffice and player-facing strings on the next request.</>}
        gate={GATE} gateNote={GATE_NOTE}>
        <HslTranslate lang={current} tab={view.tab} onTab={(t) => setView(v => ({ ...v, tab: t }))}
          onBack={() => setView({ name: "list" })}
          onCode={(t) => openCode(current, t)} />
      </HrsShell>
    );
  }

  if (view.name === "code") {
    return (
      <HrsShell title={`Edit php — ${current.name}`}
        subtitle={<>Raw source editor over the twelve files in <code>storage/lang/{current.code}/</code>. The buffer is written verbatim.</>}
        gate={GATE} gateNote={GATE_NOTE}>
        <HslCode lang={current} file={view.file} onFile={(f) => setView(v => ({ ...v, file: f }))}
          onBack={() => setView({ name: "list" })}
          onTranslate={(t) => openTranslate(current, t)} />
      </HrsShell>
    );
  }

  const COLS = [
    { key: "id", label: "ID", width: 90, sortable: true, firstDir: "desc" },
    {
      key: "name", label: "Name", align: "left", sortable: true, firstDir: "asc",
      render: r => (
        <button className="hsl-namelink" onClick={() => setModal({ edit: r })} title="Edit language">
          <span>{r.name}</span>
          <code>{r.code}</code>
          <Icon name="chevron_right" size={12} />
        </button>
      ),
    },
    {
      /* Hardcoded English header on the real screen (controller L57); the
         cell renders languages.name again as the drill-down link. */
      key: "translate", label: "Translate", align: "left", sortable: true, firstDir: "asc",
      render: r => (
        <button className="hsl-tlink" onClick={() => openTranslate(r)} title={`GET /languages/translate/${r.id}`}>
          <Icon name="list" size={12} /> {r.name}
        </button>
      ),
    },
    {
      key: "actions", label: "Actions", width: 120,
      render: r => (
        <div className="hsl-acts">
          <button className="hsl-act hsl-act--danger" title="Delete" onClick={() => setModal({ del: r })}><Icon name="trash" size={13} /></button>
          <button className="hsl-act" title="Edit" onClick={() => setModal({ edit: r })}><Icon name="edit" size={13} /></button>
        </div>
      ),
    },
  ];

  return (
    <HrsShell
      title="Languages"
      subtitle={<>Rows in the <code>languages</code> table, plus the two editors that write the runtime catalogues under <code>storage/lang/</code>.</>}
      gate={GATE}
      gateNote={GATE_NOTE}
      actions={<button className="btn btn--primary btn--sm" onClick={() => setModal("new")}><Icon name="plus" size={13} /> New language</button>}
      explainer={{
        title: "What this is, in plain English",
        bullets: [
          <><b>The list</b> is the <code>languages</code> table and nothing else — four columns, two text filters, no status column and no export, exactly like the real screen. <code>backend = 1</code> is what lets a language be picked in the admin topbar; the picker only sets a session locale, it does not change anyone's profile.</>,
          <><b>Translate</b> opens a two-column grid: English on the left (from <code>storage/lang/en/&lt;file&gt;.php</code>), the target language on the right. Saving <b>regenerates the whole file</b>, and empty boxes are skipped — so clearing a value deletes the key and its label falls back to the raw <code>backend.&#123;key&#125;</code> string.</>,
          <><b>Edit php</b> is a raw source editor over all twelve lang files. It writes PHP verbatim with no syntax check and no role check beyond login + 2FA — nine of the twelve files have no other editor.</>,
          <><b>These edits are live.</b> The runtime translation path is <code>storage/lang/</code> (not <code>resources/lang/</code>, which is bypassed), so a save changes what operators and players see on the very next request. The <i>language list</i> itself is cached for 24h and is not busted on save.</>,
        ],
        body: <>Translations are per language <em>code</em> — the folder name under <code>storage/lang/</code>. Content translation (blogs, FAQs, banners, SEO blocks) never goes through these files; it lives in its own tables and is edited on the CMS screens.</>,
      }}
    >
      <HrsFilters
        fields={HSL_FIELDS}
        values={draft}
        onChange={(k, v) => setDraft(d => ({ ...d, [k]: v }))}
        onSearch={(v) => { setApplied(v); setPage(0); }}
        onReset={() => { setDraft({ id: "", name: "" }); setApplied({ id: "", name: "" }); setPage(0); }}
        resultLabel={<>{filtered.length} of {rows.length}</>}
      />

      <HrsAsync state={feed} skeletonRows={8} skeletonCols={6}
                empty="No locales configured yet. 006_content.sql seeds ten.">
        {() => (<>
      <HrsTable
        columns={COLS}
        rows={paged}
        rowKey="id"
        sort={sort}
        onSort={(s) => { setSort(s); setPage(0); }}
        empty="No language matches these filters."
        renderCard={r => (
          <>
            <div className="hrs-card__top">
              <b>{r.name}</b>
              <span className="hsl-cardcode">{r.code}</span>
            </div>
            <div className="hrs-card__grid">
              <span>ID</span><b>{r.id}</b>
              <span>Locale</span><b>{r.locale}</b>
            </div>
            <div className="hsl-cardacts">
              <button className="btn btn--secondary btn--sm" onClick={() => openTranslate(r)}><Icon name="list" size={12} /> Translate</button>
              <button className="btn btn--ghost btn--sm" onClick={() => setModal({ edit: r })}><Icon name="edit" size={12} /> Edit</button>
              <button className="btn btn--ghost btn--sm" onClick={() => setModal({ del: r })}><Icon name="trash" size={12} /> Delete</button>
            </div>
          </>
        )}
      />
        </>)}
      </HrsAsync>

      <HrsPager page={page} pageSize={pageSize} total={sorted.length}
        onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(0); }} sizes={[5, 10, 25, 50]} />

      {/* No export block: the real screen has none. */}

      {modal === "new" && <HslFormModal onClose={() => setModal(null)} onSave={doSave} />}
      {modal && modal.edit && <HslFormModal row={modal.edit} onClose={() => setModal(null)} onSave={doSave} />}
      {modal && modal.del && <HslDeleteModal row={modal.del} onClose={() => setModal(null)} onConfirm={() => doDelete(modal.del)} />}
    </HrsShell>
  );
};

window.SetLanguages = SetLanguages;
