/* RGT Arena i18n — RU / KK / EN
   Loaded before data.jsx so GAMES can call t() at module init.
   Locale persists to localStorage; subscribers re-render via useLocale(). */

const LOCALES = ['en', 'ru', 'kk'];
const LOCALE_KEY = 'rgt-arena-locale-v1';

const DICT = {
  en: {
    'app.title': 'RGT Arena',
    'app.brand_name': 'Robotek Grand Tournament',
    'app.brand_sub': '· play the season',
    'hub.eyebrow': 'Season 2025–2026 · 7 categories',
    'hub.title_a': 'Run the',
    'hub.title_b': 'full season',
    'hub.blurb': 'Seven autonomous-robot challenges, one operator. Beat the clock, master the rules, climb the board.',
    'hub.progress.cats': '{n}/{t} categories',
    'hub.progress.pct': '{n}% complete',
    'hub.lb.title': 'Global leaderboard',
    'hub.lb.subtitle': 'Top 8 scores',
    'hub.lb.you_suffix': ' · you',
    'hub.level.best': 'BEST',
    'hub.level.ready': 'READY',
    'hub.level.limit': '{n}:00 limit',
    'hub.level.max': 'max {n}',
    'hub.level.age': 'age {age}',
    'hub.lang.label': 'Lang',
    'profile.welcome_title': 'Sign in to compete',
    'profile.welcome_blurb': 'Pick a nickname or your full name — it will appear on the leaderboard.',
    'profile.placeholder': 'Your name or nickname',
    'profile.save': 'Save & start',
    'profile.edit': 'Change name',
    'profile.invalid': 'Enter at least 2 characters',
    'profile.cancel': 'Cancel',
    'hud.score': 'Score',
    'hud.status': 'Status',
    'hud.time': 'Time',
    'hud.step': 'Step',
    'hud.zones': 'Zones',
    'hud.balls': 'Balls',
    'hud.carrying': 'Carrying',
    'hud.cargo': 'Cargo',
    'hud.placed': 'Placed',
    'hud.vs_opp': 'vs Opp',
    'cargo.empty': 'empty',
    'cargo.flag': 'flag',
    'cargo.returning': 'returning',
    'tut.eyebrow': 'Mission · {n}:00 limit',
    'tut.start': 'Start attempt',
    'tut.retry': 'Retry attempt',
    'tut.skip': 'Skip this level →',
    'result.ribbon': '{name} · Round complete',
    'result.timeout': 'Time’s up',
    'result.dq': 'Disqualified',
    'result.complete': 'Run complete',
    'result.timeout_sub': 'You ran out of time. Score logged.',
    'result.dq_sub': 'Wheels off the line. Points kept up to that moment.',
    'result.finish_sub': 'Finished in {time}',
    'result.max_suffix': '· max {n}',
    'result.best_chip': '★ New personal best',
    'result.next': 'Next level →',
    'result.last': 'You finished all levels',
    'result.retry': 'Retry this level',
    'result.skip_hub': 'Skip to hub',
    'result.total': 'TOTAL',
    'result.opp_scored': 'Opponent scored',
    'btn.steer': 'Drive',
    'btn.steer_aim': 'Drive / Aim',
    'btn.label_a': 'Action',
    'btn.pickup': 'Pick up',
    'btn.drop': 'Drop',
    'btn.pick_chip': 'Pick chip',
    'btn.pick_container': 'Pick container',
    'btn.drop_flag': 'Drop flag',
    'btn.pick_flag': 'Pick flag',
    'btn.shoot': 'Shoot',
    'btn.aim_reset': 'Aim →',
    'btn.aim_reset_hint': 'reset',
    'btn.aim_hint_fire': 'fire ball',
    'btn.finish': 'Finish',
    'btn.finish_hint': 'at start zone',
    'btn.finish_home': 'home',
    'btn.identify': 'Identify',
    'btn.identify_hint': 'scan',
    'btn.main_basket': 'Main (3pt)',
    'btn.side_basket': 'Side (1pt)',
    'btn.hint_to_arrow': '→ {dest}',
    'btn.hint_at': 'at {dest}',
    'btn.hint_cell': 'cell {n}',
    'btn.hint_slot': 'slot {id}',
    'btn.hint_color_wall': '{color} wall',
    'btn.hint_find_can': 'find can',
    'btn.hint_find_slot': 'find slot',
    'btn.hint_find_chip': 'find chip',
    'btn.hint_find_wall': 'find wall',
    'btn.hint_drive': 'drive to target',
    'btn.hint_balls_n': '{n} ball',
    'btn.hint_balls_p': '{n} balls',
    'inverse.helper': 'Push **up** to follow path\nPure driving — no buttons',
    'toast.drive_closer': 'Drive closer to a zone',
    'toast.no_cube_here': 'No cube here',
    'toast.go_to': 'Go to {dest}',
    'toast.wrong_zone_restart': 'Wrong zone — restart',
    'toast.picked_color_cube': 'Picked {color} cube',
    'toast.picked_can': 'Picked can from cell {n}',
    'toast.no_can_nearby': 'No can nearby',
    'toast.no_slot_nearby': 'No empty slot nearby',
    'toast.drive_finish': 'Drive to start zone to finish',
    'toast.drop_can_first': 'Drop your can first',
    'toast.aim_reset': 'Aim reset →',
    'toast.no_ball': 'No ball — drive over one',
    'toast.no_chip_nearby': 'No chip nearby',
    'toast.no_wall_nearby': 'No empty wall nearby',
    'toast.picked_color': 'Picked {color}',
    'toast.nothing_to_do': 'Nothing to do here',
    'toast.identify_offline': 'Identify scanner offline here',
    'toast.container_is': 'Container is {color}',
    'toast.carrying_color': 'You’re carrying a {color} container',
    'toast.return_via': 'Return via opposite corridor: {door}',
    'toast.delta': '{delta}',
    'breakdown.place': 'Place {color} → {zone}',
    'breakdown.wrong_zone': 'Wrong zone',
    'breakdown.restart': 'Restart penalty',
    'breakdown.zones_x10': '{n} zones × 10',
    'breakdown.target_down': 'Target {n} down',
    'breakdown.balls_unused': '{n} ball unused × 5',
    'breakdown.balls_unused_p': '{n} balls unused × 5',
    'breakdown.finish_bonus': 'Finish bonus',
    'breakdown.can_in': 'Can in {id}',
    'breakdown.can_in_boundary': 'Can in {id} (boundary)',
    'breakdown.finish_start': 'Finish in start zone',
    'breakdown.container_picked': 'Container picked up',
    'breakdown.container_to': 'Container delivered to {dest}',
    'breakdown.wrong_corridor': 'Wrong corridor ({dest})',
    'breakdown.flag_picked': 'Flag picked up',
    'breakdown.flag_correct': 'Flag via correct (opposite) corridor',
    'breakdown.flag_wrong': 'Flag via wrong corridor',
    'breakdown.zigzag_fwd': 'ZIGZAG (forward)',
    'breakdown.zigzag_back': 'ZIGZAG (back)',
    'breakdown.ramp_fwd': 'RAMP (forward)',
    'breakdown.ramp_back': 'RAMP (back)',
    'breakdown.gradient_fwd': 'GRAD (forward)',
    'breakdown.gradient_back': 'GRAD (back)',
    'breakdown.inverse_fwd': 'INV (forward)',
    'breakdown.inverse_back': 'INV (back)',
    'breakdown.main_basket': 'main basket',
    'breakdown.side_basket': 'side basket',
    'breakdown.win_bonus': 'Win bonus',
    'breakdown.chip_placed': '{color} placed',
    'breakdown.chip_boundary': '{color} on boundary',
    'breakdown.chip_wrong': '{color} on wrong wall',
    'color.red': 'red',
    'color.blue': 'blue',
    'color.black': 'black',
    'color.yellow': 'yellow',
    'color.white': 'white',
    'g1.name': 'Robotek',
    'g1.tagline': 'Cube relocation logistics',
    'g1.blurb': 'Move red and blue cubes between zones A and B in the right order — as many cycles as you can in 3:00.',
    'g1.tut1': 'Tap the **highlighted cell** to move the robot there. Pick up cubes and place them following the sequence.',
    'g1.tut2': 'Cycle: **A1→B1, B2→A2, B1→A1, A2→B2**. Repeat as long as time allows.',
    'g1.tut3': 'Wrong placement triggers a **5-second restart** and costs −5 points. Don’t drop the cube outside the zone.',
    'g1.s1': 'Container fully in zone',
    'g1.s2': 'Container touches zone edge',
    'g1.s3': 'Container outside / wrong zone',
    'g1.s4': 'Robot restart',
    'g2.name': 'Inverse Line',
    'g2.tagline': 'Race the contrast track',
    'g2.blurb': 'Steer the robot along the checker line — black on white, white on black. 12 zones, fastest time wins.',
    'g2.tut1': 'Use the **steering pad** on the left to keep the robot centered on the line.',
    'g2.tut2': 'Cross all **12 contrast zones** to score the maximum 120 points.',
    'g2.tut3': 'If your wheelbase fully leaves the line, you’re disqualified — points stay, run ends.',
    'g2.s1': 'Each zone crossed',
    'g2.s2': 'All 12 zones (max)',
    'g2.s3': 'Wheels off line',
    'g3.name': 'Delivery Robot',
    'g3.tagline': 'Cans to the dock',
    'g3.blurb': 'Pick up 4 white cans from cells 1–9 and place them in delivery zones A–F. Return to base. 2:00.',
    'g3.tut1': 'Drive to a numbered cell to **pick up a can**. Carry one or more.',
    'g3.tut2': 'Drop into delivery slots **A–C** or **D–F**. Max 2 cans per zone.',
    'g3.tut3': 'Return to the **finish zone** for +10. Max score caps at 50.',
    'g3.s1': 'Can fully in slot',
    'g3.s2': 'Can on slot edge',
    'g3.s3': 'Stop in finish zone',
    'g3.s4': '>3 cans in one zone',
    'g3.s5': 'Maximum total',
    'g4.name': 'Mergen',
    'g4.tagline': 'Knock-down marksman',
    'g4.blurb': 'Aim ping-pong balls from the shooting zone, knock down 3 targets across the river, return to start. 2:00.',
    'g4.tut1': 'Tap & drag in the **shooting zone** to aim and release a ball.',
    'g4.tut2': 'Don’t let balls land in the **river** — they don’t count, even if they hit.',
    'g4.tut3': 'Save unused balls for bonus points after clearing all 3 targets.',
    'g4.s1': 'Each target down',
    'g4.s2': 'Finish in start zone',
    'g4.s3': 'Each unused ball (if all targets down)',
    'g4.s4': 'Shot fired outside shooting zone',
    'g4.s5': 'Maximum total',
    'g5.name': 'Flag Delivery',
    'g5.tagline': 'Obstacle course relay',
    'g5.blurb': 'Zigzag, ramp, gradient, color sort, flag pickup, return through the right corridor. 2:00.',
    'g5.tut1': 'Press **Forward** to advance through the course stages in order.',
    'g5.tut2': 'At the container, pick up **blue → Zone B** (right corridor) or **red → Zone C** (left corridor).',
    'g5.tut3': 'Pick up the **flag** and return through the *opposite* corridor.',
    'g5.s1': 'Zigzag (each way)',
    'g5.s2': 'Ramp (each way)',
    'g5.s3': 'Gradient (each way)',
    'g5.s4': 'Inverse zone',
    'g5.s5': 'Container in correct zone',
    'g5.s6': 'Flag pickup + delivery',
    'g5.s7': 'Finish in start zone',
    'g5.s8': 'Wrong corridor',
    'g5.s9': 'Knocked obstacle',
    'g6.name': 'Sorting Robot',
    'g6.tagline': 'Color match against the wall',
    'g6.blurb': 'Five colored chips, five walls. Place each chip next to its matching wall. 2:00.',
    'g6.tut1': 'Drag each chip from the loader to the **matching wall color**.',
    'g6.tut2': 'Place inside the circle without touching the wall: **+10**. On the boundary: **+5**.',
    'g6.tut3': 'Wrong wall or moved wall: **−5**.',
    'g6.s1': 'Chip correctly placed inside circle',
    'g6.s2': 'Chip touching circle boundary',
    'g6.s3': 'Chip in wrong circle',
    'g6.s4': 'Robot moves a wall',
    'g6.s5': 'Maximum total',
    'g7.name': 'RoboBasketball',
    'g7.tagline': 'Score on the opponent',
    'g7.blurb': 'Collect orange balls from your half, throw at the opponent baskets. Don’t cross the red line. 2:00.',
    'g7.tut1': 'Drive on **your half only**. Touching the red line ends the match.',
    'g7.tut2': 'Collect a ball, then **tap the basket** to throw. Main basket = 3, side basket = 1.',
    'g7.tut3': 'Beat the opponent’s bot before time runs out.',
    'g7.s1': 'Ball in main basket',
    'g7.s2': 'Ball in side basket',
    'g7.s3': 'Win bonus',
    'g7.s4': 'Cross red line / opp half',
    'g7.s4_v': 'Tech defeat',
  },
  ru: {
    'app.title': 'RGT Арена',
    'app.brand_name': 'Robotek Grand Tournament',
    'app.brand_sub': '· играй сезон',
    'hub.eyebrow': 'Сезон 2025–2026 · 7 категорий',
    'hub.title_a': 'Пройди',
    'hub.title_b': 'весь сезон',
    'hub.blurb': 'Семь автономных робот-челленджей, один оператор. Уложись во время, освой правила, забирайся в топ.',
    'hub.progress.cats': '{n}/{t} категорий',
    'hub.progress.pct': 'готово {n}%',
    'hub.lb.title': 'Глобальный лидерборд',
    'hub.lb.subtitle': 'Топ-8 результатов',
    'hub.lb.you_suffix': ' · ты',
    'hub.level.best': 'РЕКОРД',
    'hub.level.ready': 'ГОТОВ',
    'hub.level.limit': 'лимит {n}:00',
    'hub.level.max': 'макс {n}',
    'hub.level.age': 'возраст {age}',
    'hub.lang.label': 'Язык',
    'profile.welcome_title': 'Представься',
    'profile.welcome_blurb': 'Введи никнейм или ФИ — он появится в лидерборде.',
    'profile.placeholder': 'Имя или никнейм',
    'profile.save': 'Сохранить и играть',
    'profile.edit': 'Сменить имя',
    'profile.invalid': 'Минимум 2 символа',
    'profile.cancel': 'Отмена',
    'hud.score': 'Очки',
    'hud.status': 'Статус',
    'hud.time': 'Время',
    'hud.step': 'Шаг',
    'hud.zones': 'Зоны',
    'hud.balls': 'Мячи',
    'hud.carrying': 'Несёт',
    'hud.cargo': 'Груз',
    'hud.placed': 'Уложено',
    'hud.vs_opp': 'vs Опп',
    'cargo.empty': 'пусто',
    'cargo.flag': 'флаг',
    'cargo.returning': 'возврат',
    'tut.eyebrow': 'Миссия · лимит {n}:00',
    'tut.start': 'Начать попытку',
    'tut.retry': 'Повторить попытку',
    'tut.skip': 'Пропустить уровень →',
    'result.ribbon': '{name} · раунд завершён',
    'result.timeout': 'Время вышло',
    'result.dq': 'Дисквалификация',
    'result.complete': 'Заезд завершён',
    'result.timeout_sub': 'Время кончилось. Очки сохранены.',
    'result.dq_sub': 'Колёса сошли с линии. Очки до этого момента сохранены.',
    'result.finish_sub': 'Финиш за {time}',
    'result.max_suffix': '· макс {n}',
    'result.best_chip': '★ Новый личный рекорд',
    'result.next': 'Следующий уровень →',
    'result.last': 'Все уровни пройдены',
    'result.retry': 'Повторить уровень',
    'result.skip_hub': 'В хаб',
    'result.total': 'ИТОГО',
    'result.opp_scored': 'Соперник набрал',
    'btn.steer': 'Руль',
    'btn.steer_aim': 'Руль / Прицел',
    'btn.label_a': 'Действие',
    'btn.pickup': 'Поднять',
    'btn.drop': 'Положить',
    'btn.pick_chip': 'Взять фишку',
    'btn.pick_container': 'Взять контейнер',
    'btn.drop_flag': 'Положить флаг',
    'btn.pick_flag': 'Взять флаг',
    'btn.shoot': 'Стрелять',
    'btn.aim_reset': 'Прицел →',
    'btn.aim_reset_hint': 'сброс',
    'btn.aim_hint_fire': 'выпустить мяч',
    'btn.finish': 'Финиш',
    'btn.finish_hint': 'в стартовой зоне',
    'btn.finish_home': 'домой',
    'btn.identify': 'Сканер',
    'btn.identify_hint': 'опознать',
    'btn.main_basket': 'Главная (3)',
    'btn.side_basket': 'Боковая (1)',
    'btn.hint_to_arrow': '→ {dest}',
    'btn.hint_at': 'у {dest}',
    'btn.hint_cell': 'клетка {n}',
    'btn.hint_slot': 'слот {id}',
    'btn.hint_color_wall': 'стена {color}',
    'btn.hint_find_can': 'найди банку',
    'btn.hint_find_slot': 'найди слот',
    'btn.hint_find_chip': 'найди фишку',
    'btn.hint_find_wall': 'найди стену',
    'btn.hint_drive': 'едь к цели',
    'btn.hint_balls_n': '{n} мяч',
    'btn.hint_balls_p': '{n} мячей',
    'inverse.helper': 'Жми **вверх** чтобы ехать по линии\nЧистое вождение — без кнопок',
    'toast.drive_closer': 'Подъезжай ближе к зоне',
    'toast.no_cube_here': 'Здесь нет куба',
    'toast.go_to': 'Едь к {dest}',
    'toast.wrong_zone_restart': 'Не та зона — рестарт',
    'toast.picked_color_cube': 'Поднял {color} куб',
    'toast.picked_can': 'Поднял банку из клетки {n}',
    'toast.no_can_nearby': 'Рядом нет банок',
    'toast.no_slot_nearby': 'Нет пустого слота рядом',
    'toast.drive_finish': 'Вернись в стартовую зону чтобы финишировать',
    'toast.drop_can_first': 'Сначала положи банку',
    'toast.aim_reset': 'Прицел сброшен →',
    'toast.no_ball': 'Нет мяча — проедь по нему',
    'toast.no_chip_nearby': 'Рядом нет фишек',
    'toast.no_wall_nearby': 'Нет свободной стены рядом',
    'toast.picked_color': 'Поднял {color}',
    'toast.nothing_to_do': 'Здесь нечего делать',
    'toast.identify_offline': 'Сканер недоступен',
    'toast.container_is': 'Контейнер {color}',
    'toast.carrying_color': 'У тебя {color} контейнер',
    'toast.return_via': 'Возврат через противоположный коридор: {door}',
    'toast.delta': '{delta}',
    'breakdown.place': 'Положить {color} → {zone}',
    'breakdown.wrong_zone': 'Не та зона',
    'breakdown.restart': 'Штраф за рестарт',
    'breakdown.zones_x10': '{n} зон × 10',
    'breakdown.target_down': 'Цель {n} сбита',
    'breakdown.balls_unused': '{n} мяч не использован × 5',
    'breakdown.balls_unused_p': '{n} мячей не использовано × 5',
    'breakdown.finish_bonus': 'Бонус за финиш',
    'breakdown.can_in': 'Банка в {id}',
    'breakdown.can_in_boundary': 'Банка в {id} (на границе)',
    'breakdown.finish_start': 'Финиш в стартовой зоне',
    'breakdown.container_picked': 'Контейнер поднят',
    'breakdown.container_to': 'Контейнер доставлен в {dest}',
    'breakdown.wrong_corridor': 'Неверный коридор ({dest})',
    'breakdown.flag_picked': 'Флаг поднят',
    'breakdown.flag_correct': 'Флаг через верный (противоположный) коридор',
    'breakdown.flag_wrong': 'Флаг через неверный коридор',
    'breakdown.zigzag_fwd': 'ЗИГЗАГ (вперёд)',
    'breakdown.zigzag_back': 'ЗИГЗАГ (назад)',
    'breakdown.ramp_fwd': 'ТРАМПЛИН (вперёд)',
    'breakdown.ramp_back': 'ТРАМПЛИН (назад)',
    'breakdown.gradient_fwd': 'ГРАДИЕНТ (вперёд)',
    'breakdown.gradient_back': 'ГРАДИЕНТ (назад)',
    'breakdown.inverse_fwd': 'ИНВЕРСИЯ (вперёд)',
    'breakdown.inverse_back': 'ИНВЕРСИЯ (назад)',
    'breakdown.main_basket': 'главная корзина',
    'breakdown.side_basket': 'боковая корзина',
    'breakdown.win_bonus': 'Бонус за победу',
    'breakdown.chip_placed': '{color} установлен',
    'breakdown.chip_boundary': '{color} на границе',
    'breakdown.chip_wrong': '{color} не на той стене',
    'color.red': 'красный',
    'color.blue': 'синий',
    'color.black': 'чёрный',
    'color.yellow': 'жёлтый',
    'color.white': 'белый',
    'g1.name': 'Роботек',
    'g1.tagline': 'Логистика кубов',
    'g1.blurb': 'Перемещай красные и синие кубы между зонами A и B в правильном порядке — максимум циклов за 3:00.',
    'g1.tut1': 'Касайся **подсвеченной клетки** чтобы переехать туда. Поднимай кубы и расставляй по последовательности.',
    'g1.tut2': 'Цикл: **A1→B1, B2→A2, B1→A1, A2→B2**. Повторяй пока есть время.',
    'g1.tut3': 'Ошибка → **5-секундный рестарт** и −5. Не роняй кубы за пределы зоны.',
    'g1.s1': 'Контейнер полностью в зоне',
    'g1.s2': 'Контейнер на границе зоны',
    'g1.s3': 'Контейнер вне / в чужой зоне',
    'g1.s4': 'Рестарт робота',
    'g2.name': 'Инверсная линия',
    'g2.tagline': 'Гонка по контрастной трассе',
    'g2.blurb': 'Веди робота по шахматной линии — чёрный на белом, белый на чёрном. 12 зон, побеждает быстрейший.',
    'g2.tut1': 'Используй **руль слева** чтобы держать робота на линии.',
    'g2.tut2': 'Пересеки все **12 зон контраста** для максимума 120 очков.',
    'g2.tut3': 'Если колёсная база полностью сошла с линии — DQ. Очки до этого сохраняются.',
    'g2.s1': 'Каждая зона',
    'g2.s2': 'Все 12 зон (макс)',
    'g2.s3': 'Колёса с линии',
    'g3.name': 'Робот-доставщик',
    'g3.tagline': 'Банки в док',
    'g3.blurb': 'Подбери 4 белые банки из клеток 1–9 и расставь по зонам A–F. Вернись на базу. 2:00.',
    'g3.tut1': 'Едь к нумерованной клетке чтобы **взять банку**. Можно нести несколько.',
    'g3.tut2': 'Бросай в слоты **A–C** или **D–F**. Максимум 2 банки на зону.',
    'g3.tut3': 'Вернись в **зону финиша** за +10. Максимум — 50.',
    'g3.s1': 'Банка полностью в слоте',
    'g3.s2': 'Банка на границе слота',
    'g3.s3': 'Стоп в зоне финиша',
    'g3.s4': '>3 банок в одной зоне',
    'g3.s5': 'Максимум',
    'g4.name': 'Мерген',
    'g4.tagline': 'Стрелок по мишеням',
    'g4.blurb': 'Целься мячиками из зоны стрельбы, сбей 3 мишени за рекой, вернись на старт. 2:00.',
    'g4.tut1': 'Тапни и тяни в **зоне стрельбы** чтобы прицелиться и пустить мяч.',
    'g4.tut2': 'Не дай мячам упасть в **реку** — они не зачтутся, даже если попадут.',
    'g4.tut3': 'Сохраняй неиспользованные мячи для бонуса после сбития всех 3 мишеней.',
    'g4.s1': 'Каждая мишень сбита',
    'g4.s2': 'Финиш в стартовой зоне',
    'g4.s3': 'Каждый неиспользованный мяч (все мишени сбиты)',
    'g4.s4': 'Выстрел вне зоны',
    'g4.s5': 'Максимум',
    'g5.name': 'Доставка флага',
    'g5.tagline': 'Полоса препятствий',
    'g5.blurb': 'Зигзаг, трамплин, градиент, сортировка цвета, флаг, возврат через нужный коридор. 2:00.',
    'g5.tut1': 'Жми **Вперёд** чтобы продвигаться по этапам трассы по очереди.',
    'g5.tut2': 'У контейнера: **синий → зона B** (правый коридор) или **красный → зона C** (левый коридор).',
    'g5.tut3': 'Подбери **флаг** и возвращайся через *противоположный* коридор.',
    'g5.s1': 'Зигзаг (каждое направление)',
    'g5.s2': 'Трамплин (каждое направление)',
    'g5.s3': 'Градиент (каждое направление)',
    'g5.s4': 'Зона инверсии',
    'g5.s5': 'Контейнер в верной зоне',
    'g5.s6': 'Подбор + доставка флага',
    'g5.s7': 'Финиш в стартовой зоне',
    'g5.s8': 'Неверный коридор',
    'g5.s9': 'Сбитое препятствие',
    'g6.name': 'Робот-сортировщик',
    'g6.tagline': 'Подбор цвета по стене',
    'g6.blurb': 'Пять цветных фишек, пять стен. Расставь каждую к её цвету. 2:00.',
    'g6.tut1': 'Тащи каждую фишку из загрузчика к **стене того же цвета**.',
    'g6.tut2': 'Внутри круга без касания стены: **+10**. На границе: **+5**.',
    'g6.tut3': 'Не та стена или сдвинутая стена: **−5**.',
    'g6.s1': 'Фишка в круге',
    'g6.s2': 'Фишка на границе круга',
    'g6.s3': 'Фишка не в том круге',
    'g6.s4': 'Робот сдвинул стену',
    'g6.s5': 'Максимум',
    'g7.name': 'РобоБаскетбол',
    'g7.tagline': 'Забрось сопернику',
    'g7.blurb': 'Собирай оранжевые мячи на своей половине, бросай в чужие корзины. Не пересекай красную линию. 2:00.',
    'g7.tut1': 'Едь только по **своей половине**. Касание красной линии — конец матча.',
    'g7.tut2': 'Подбери мяч и **тапни корзину**. Главная = 3, боковая = 1.',
    'g7.tut3': 'Обыграй соперника до конца времени.',
    'g7.s1': 'Мяч в главной корзине',
    'g7.s2': 'Мяч в боковой корзине',
    'g7.s3': 'Бонус за победу',
    'g7.s4': 'Пересёк красную линию / половину',
    'g7.s4_v': 'Тех. поражение',
  },
  kk: {
    'app.title': 'RGT Арена',
    'app.brand_name': 'Robotek Grand Tournament',
    'app.brand_sub': '· маусымды ойна',
    'hub.eyebrow': 'Маусым 2025–2026 · 7 санат',
    'hub.title_a': 'Толық',
    'hub.title_b': 'маусымды өт',
    'hub.blurb': 'Жеті автономды робот-сынақ, бір оператор. Уақытты ұт, ережелерді меңгер, кестеге шық.',
    'hub.progress.cats': '{n}/{t} санат',
    'hub.progress.pct': '{n}% орындалды',
    'hub.lb.title': 'Жаһандық лидерборд',
    'hub.lb.subtitle': 'Үздік 8 нәтиже',
    'hub.lb.you_suffix': ' · сен',
    'hub.level.best': 'РЕКОРД',
    'hub.level.ready': 'ДАЙЫН',
    'hub.level.limit': 'шектеу {n}:00',
    'hub.level.max': 'макс {n}',
    'hub.level.age': 'жас {age}',
    'hub.lang.label': 'Тіл',
    'profile.welcome_title': 'Танысып ал',
    'profile.welcome_blurb': 'Лақап атыңды немесе ТАӘ-ыңды енгіз — лидербордта көрінеді.',
    'profile.placeholder': 'Аты немесе лақап аты',
    'profile.save': 'Сақтап ойнау',
    'profile.edit': 'Атын өзгерту',
    'profile.invalid': 'Кемінде 2 таңба',
    'profile.cancel': 'Болдырмау',
    'hud.score': 'Ұпай',
    'hud.status': 'Күй',
    'hud.time': 'Уақыт',
    'hud.step': 'Қадам',
    'hud.zones': 'Аймақ',
    'hud.balls': 'Доптар',
    'hud.carrying': 'Тасу',
    'hud.cargo': 'Жүк',
    'hud.placed': 'Қойылды',
    'hud.vs_opp': 'vs Қарсылас',
    'cargo.empty': 'бос',
    'cargo.flag': 'жалау',
    'cargo.returning': 'қайту',
    'tut.eyebrow': 'Миссия · шектеу {n}:00',
    'tut.start': 'Бастау',
    'tut.retry': 'Қайталау',
    'tut.skip': 'Деңгейді өткізіп жіберу →',
    'result.ribbon': '{name} · раунд аяқталды',
    'result.timeout': 'Уақыт бітті',
    'result.dq': 'Дисквалификация',
    'result.complete': 'Заезд аяқталды',
    'result.timeout_sub': 'Уақыт бітті. Ұпай сақталды.',
    'result.dq_sub': 'Доңғалақтар сызықтан шықты. Сол сәтке дейінгі ұпай сақталды.',
    'result.finish_sub': '{time} ішінде аяқталды',
    'result.max_suffix': '· макс {n}',
    'result.best_chip': '★ Жаңа жеке рекорд',
    'result.next': 'Келесі деңгей →',
    'result.last': 'Барлық деңгей өтті',
    'result.retry': 'Деңгейді қайталау',
    'result.skip_hub': 'Хабқа',
    'result.total': 'БАРЛЫҒЫ',
    'result.opp_scored': 'Қарсылас ұпайы',
    'btn.steer': 'Руль',
    'btn.steer_aim': 'Руль / Бағытта',
    'btn.label_a': 'Әрекет',
    'btn.pickup': 'Көтер',
    'btn.drop': 'Қой',
    'btn.pick_chip': 'Чипті ал',
    'btn.pick_container': 'Контейнерді ал',
    'btn.drop_flag': 'Жалауды қой',
    'btn.pick_flag': 'Жалауды ал',
    'btn.shoot': 'Ату',
    'btn.aim_reset': 'Бағытта →',
    'btn.aim_reset_hint': 'қалпына',
    'btn.aim_hint_fire': 'допты ат',
    'btn.finish': 'Мәре',
    'btn.finish_hint': 'старт аймағында',
    'btn.finish_home': 'үйге',
    'btn.identify': 'Сканер',
    'btn.identify_hint': 'сканерле',
    'btn.main_basket': 'Басты (3)',
    'btn.side_basket': 'Бүйір (1)',
    'btn.hint_to_arrow': '→ {dest}',
    'btn.hint_at': '{dest} жанында',
    'btn.hint_cell': '{n} ұяшық',
    'btn.hint_slot': '{id} слот',
    'btn.hint_color_wall': '{color} қабырға',
    'btn.hint_find_can': 'банканы тап',
    'btn.hint_find_slot': 'слотты тап',
    'btn.hint_find_chip': 'чипті тап',
    'btn.hint_find_wall': 'қабырғаны тап',
    'btn.hint_drive': 'нысанаға бар',
    'btn.hint_balls_n': '{n} доп',
    'btn.hint_balls_p': '{n} доп',
    'inverse.helper': '**Жоғары** басып сызық бойымен жүр\nТек руль — батырмасыз',
    'toast.drive_closer': 'Аймаққа жақын кел',
    'toast.no_cube_here': 'Мұнда куб жоқ',
    'toast.go_to': '{dest} бар',
    'toast.wrong_zone_restart': 'Қате аймақ — рестарт',
    'toast.picked_color_cube': '{color} куб алынды',
    'toast.picked_can': '{n}-ұяшықтан банка алынды',
    'toast.no_can_nearby': 'Жанында банка жоқ',
    'toast.no_slot_nearby': 'Бос слот жоқ',
    'toast.drive_finish': 'Аяқтау үшін старт аймағына қайт',
    'toast.drop_can_first': 'Алдымен банканы қой',
    'toast.aim_reset': 'Бағыт қалпына →',
    'toast.no_ball': 'Доп жоқ — үстінен өт',
    'toast.no_chip_nearby': 'Жанында чип жоқ',
    'toast.no_wall_nearby': 'Бос қабырға жоқ',
    'toast.picked_color': '{color} алынды',
    'toast.nothing_to_do': 'Мұнда істейтін нәрсе жоқ',
    'toast.identify_offline': 'Сканер қол жетімді емес',
    'toast.container_is': 'Контейнер {color}',
    'toast.carrying_color': 'Сенде {color} контейнер',
    'toast.return_via': 'Қарама-қарсы коридормен қайт: {door}',
    'toast.delta': '{delta}',
    'breakdown.place': 'Қою {color} → {zone}',
    'breakdown.wrong_zone': 'Қате аймақ',
    'breakdown.restart': 'Рестарт айыбы',
    'breakdown.zones_x10': '{n} аймақ × 10',
    'breakdown.target_down': '{n}-нысана құлады',
    'breakdown.balls_unused': '{n} доп пайдаланылмаған × 5',
    'breakdown.balls_unused_p': '{n} доп пайдаланылмаған × 5',
    'breakdown.finish_bonus': 'Мәре бонусы',
    'breakdown.can_in': '{id}-дегі банка',
    'breakdown.can_in_boundary': '{id}-дегі банка (шетте)',
    'breakdown.finish_start': 'Старт аймағында мәре',
    'breakdown.container_picked': 'Контейнер алынды',
    'breakdown.container_to': 'Контейнер {dest}-ке жеткізілді',
    'breakdown.wrong_corridor': 'Қате коридор ({dest})',
    'breakdown.flag_picked': 'Жалау алынды',
    'breakdown.flag_correct': 'Жалау дұрыс (қарама-қарсы) коридормен',
    'breakdown.flag_wrong': 'Жалау қате коридормен',
    'breakdown.zigzag_fwd': 'ЗИГЗАГ (алға)',
    'breakdown.zigzag_back': 'ЗИГЗАГ (кері)',
    'breakdown.ramp_fwd': 'РАМПА (алға)',
    'breakdown.ramp_back': 'РАМПА (кері)',
    'breakdown.gradient_fwd': 'ГРАДИЕНТ (алға)',
    'breakdown.gradient_back': 'ГРАДИЕНТ (кері)',
    'breakdown.inverse_fwd': 'ИНВЕРСИЯ (алға)',
    'breakdown.inverse_back': 'ИНВЕРСИЯ (кері)',
    'breakdown.main_basket': 'басты себет',
    'breakdown.side_basket': 'бүйір себет',
    'breakdown.win_bonus': 'Жеңіс бонусы',
    'breakdown.chip_placed': '{color} орналастырылды',
    'breakdown.chip_boundary': '{color} шетте',
    'breakdown.chip_wrong': '{color} қате қабырғада',
    'color.red': 'қызыл',
    'color.blue': 'көк',
    'color.black': 'қара',
    'color.yellow': 'сары',
    'color.white': 'ақ',
    'g1.name': 'Роботек',
    'g1.tagline': 'Куб логистикасы',
    'g1.blurb': 'A және B аймақтары арасында қызыл мен көк кубтарды дұрыс ретпен тасы — 3:00 ішінде барынша көп цикл.',
    'g1.tut1': '**Жанып тұрған ұяшықты** басу — робот сонда барады. Кубтарды алып, ретімен қой.',
    'g1.tut2': 'Цикл: **A1→B1, B2→A2, B1→A1, A2→B2**. Уақыт біткенше қайтала.',
    'g1.tut3': 'Қате қою — **5-секунд рестарт** және −5. Кубты аймақтан тыс тастама.',
    'g1.s1': 'Контейнер толық аймақта',
    'g1.s2': 'Контейнер аймақ шетінде',
    'g1.s3': 'Контейнер тыс / қате аймақта',
    'g1.s4': 'Робот рестарты',
    'g2.name': 'Инверсті сызық',
    'g2.tagline': 'Контраст тректе жарыс',
    'g2.blurb': 'Шахмат сызығымен жүргіз — ақта қара, қарада ақ. 12 аймақ, ең жылдамы жеңеді.',
    'g2.tut1': 'Сол жақтағы **рульмен** роботты сызық бойында ұста.',
    'g2.tut2': '**12 контраст аймақтың** бәрін кесіп, 120 ұпай ал.',
    'g2.tut3': 'Доңғалақ базасы сызықтан толық шықса — DQ. Сол сәтке дейінгі ұпай сақталады.',
    'g2.s1': 'Әр аймақ кесілген',
    'g2.s2': 'Барлық 12 аймақ (макс)',
    'g2.s3': 'Доңғалақтар сызықтан',
    'g3.name': 'Жеткізуші робот',
    'g3.tagline': 'Банкалар докқа',
    'g3.blurb': '1–9 ұяшықтан 4 ақ банканы ал да, A–F аймақтарына қой. Базаға қайт. 2:00.',
    'g3.tut1': 'Нөмірленген ұяшыққа барып **банка ал**. Бірнешеуін тасуға болады.',
    'g3.tut2': '**A–C** немесе **D–F** слоттарына таста. Аймаққа максимум 2 банка.',
    'g3.tut3': '**Мәре аймағына** қайтсаң +10. Максимум — 50.',
    'g3.s1': 'Банка толық слотта',
    'g3.s2': 'Банка слот шетінде',
    'g3.s3': 'Мәре аймағында тоқтау',
    'g3.s4': 'Бір аймақта >3 банка',
    'g3.s5': 'Максимум',
    'g4.name': 'Мерген',
    'g4.tagline': 'Нысана құлатушы',
    'g4.blurb': 'Ату аймағынан доптарды бағыттап, өзеннің арғы бетіндегі 3 нысананы құлат, стартқа қайт. 2:00.',
    'g4.tut1': '**Ату аймағында** басып-сүйрелеп бағыттай да доп жібер.',
    'g4.tut2': 'Доптар **өзенге** түспесін — тиіп те, есептелмейді.',
    'g4.tut3': '3 нысана құлағаннан кейін қалған доптар бонус ұпай береді.',
    'g4.s1': 'Әр құлаған нысана',
    'g4.s2': 'Старт аймағында мәре',
    'g4.s3': 'Әр пайдаланылмаған доп (барлық нысана құлады)',
    'g4.s4': 'Аймақтан тыс ату',
    'g4.s5': 'Максимум',
    'g5.name': 'Жалау жеткізу',
    'g5.tagline': 'Кедергілер трассасы',
    'g5.blurb': 'Зигзаг, рампа, градиент, түс іріктеу, жалау, дұрыс коридормен қайт. 2:00.',
    'g5.tut1': 'Трасса кезеңдерін бір-бірлеп өту үшін **Алға** бас.',
    'g5.tut2': 'Контейнерде: **көк → B аймағы** (оң коридор) немесе **қызыл → C аймағы** (сол коридор).',
    'g5.tut3': '**Жалауды** ал да *қарама-қарсы* коридормен қайт.',
    'g5.s1': 'Зигзаг (әр бағыт)',
    'g5.s2': 'Рампа (әр бағыт)',
    'g5.s3': 'Градиент (әр бағыт)',
    'g5.s4': 'Инверсия аймағы',
    'g5.s5': 'Контейнер дұрыс аймақта',
    'g5.s6': 'Жалау алу + жеткізу',
    'g5.s7': 'Старт аймағында мәре',
    'g5.s8': 'Қате коридор',
    'g5.s9': 'Кедергі құлатылды',
    'g6.name': 'Сұрыптаушы робот',
    'g6.tagline': 'Қабырғамен түс сәйкес',
    'g6.blurb': 'Бес түсті чип, бес қабырға. Әр чипті өз түсіне қой. 2:00.',
    'g6.tut1': 'Әр чипті жүктегіштен **сәйкес түсті қабырғаға** сүйре.',
    'g6.tut2': 'Қабырғаға тимей шеңберде: **+10**. Шетте: **+5**.',
    'g6.tut3': 'Қате қабырға немесе жылжытылған қабырға: **−5**.',
    'g6.s1': 'Чип шеңбер ішінде дұрыс',
    'g6.s2': 'Чип шеңбер шетінде',
    'g6.s3': 'Чип қате шеңберде',
    'g6.s4': 'Робот қабырғаны жылжытты',
    'g6.s5': 'Максимум',
    'g7.name': 'РобоБаскет',
    'g7.tagline': 'Қарсыласқа лақтыр',
    'g7.blurb': 'Өз жартыңнан қызғылт сары доптарды жинап, қарсылас себеттеріне лақтыр. Қызыл сызықтан өтпе. 2:00.',
    'g7.tut1': 'Тек **өз жартыңда** жүргіз. Қызыл сызыққа тию — матч соңы.',
    'g7.tut2': 'Допты ал да **себетті бас**. Басты = 3, бүйір = 1.',
    'g7.tut3': 'Уақыт біткенше қарсылас ботын ұт.',
    'g7.s1': 'Доп басты себетте',
    'g7.s2': 'Доп бүйір себетте',
    'g7.s3': 'Жеңіс бонусы',
    'g7.s4': 'Қызыл сызық / қарсылас жартысы',
    'g7.s4_v': 'Тех. жеңіліс',
  },
};

let _currentLocale = (() => {
  try {
    const saved = localStorage.getItem(LOCALE_KEY);
    if (saved && LOCALES.includes(saved)) return saved;
  } catch {}
  // Fall back to browser language hint, default 'en'
  try {
    const nav = (navigator.language || '').slice(0, 2).toLowerCase();
    if (LOCALES.includes(nav)) return nav;
  } catch {}
  return 'en';
})();

const _localeSubs = new Set();

function getLocale() { return _currentLocale; }
function setLocale(loc) {
  if (!LOCALES.includes(loc)) return;
  _currentLocale = loc;
  try { localStorage.setItem(LOCALE_KEY, loc); } catch {}
  _localeSubs.forEach(fn => { try { fn(loc); } catch {} });
}

function t(key, params) {
  const dict = DICT[_currentLocale] || DICT.en;
  let s = dict[key];
  if (s == null) s = (DICT.en[key] != null ? DICT.en[key] : key);
  if (params) {
    for (const k of Object.keys(params)) {
      s = s.split('{' + k + '}').join(String(params[k]));
    }
  }
  return s;
}

/* Render a translation string with **bold** and *italic* markers as JSX. */
function tx(key, params) {
  const raw = t(key, params);
  return parseInline(raw);
}

function parseInline(str) {
  if (!str) return null;
  const lines = str.split('\n');
  const out = [];
  lines.forEach((line, li) => {
    if (li > 0) out.push(<br key={'br-' + li} />);
    let i = 0;
    let buf = '';
    const flush = () => { if (buf) { out.push(buf); buf = ''; } };
    while (i < line.length) {
      if (line[i] === '*' && line[i+1] === '*') {
        const end = line.indexOf('**', i + 2);
        if (end > -1) {
          flush();
          out.push(<b key={li + '-b-' + i}>{line.slice(i + 2, end)}</b>);
          i = end + 2;
          continue;
        }
      }
      if (line[i] === '*') {
        const end = line.indexOf('*', i + 1);
        if (end > -1) {
          flush();
          out.push(<i key={li + '-i-' + i}>{line.slice(i + 1, end)}</i>);
          i = end + 1;
          continue;
        }
      }
      buf += line[i];
      i++;
    }
    flush();
  });
  return <>{out.map((n, i) => typeof n === 'string' ? <React.Fragment key={i}>{n}</React.Fragment> : n)}</>;
}

/* React hook — components subscribe to locale changes and re-render. */
function useLocale() {
  const [, force] = React.useState(0);
  React.useEffect(() => {
    const fn = () => force(v => v + 1);
    _localeSubs.add(fn);
    return () => _localeSubs.delete(fn);
  }, []);
  return [_currentLocale, setLocale];
}

window.LOCALES = LOCALES;
window.t = t;
window.tx = tx;
window.getLocale = getLocale;
window.setLocale = setLocale;
window.useLocale = useLocale;
window._localeSubs = _localeSubs;
