// ==UserScript== // @name Steam Region Block Bypass // @name:ru Steam Region Block Bypass — обход региональной блокировки // @name:zh-CN Steam Region Block Bypass — 区域限制绕过 // @name:es Steam Region Block Bypass — bypass de región // @name:pt-BR Steam Region Block Bypass — bypass de região // @name:de Steam Region Block Bypass — Regionsperre umgehen // @name:fr Steam Region Block Bypass — contournement régional // @name:ja Steam Region Block Bypass — 地域制限バイパス // @name:ko Steam Region Block Bypass — 지역 제한 우회 // @name:pl Steam Region Block Bypass — obejście blokady regionu // @namespace https://github.com/NemoKing1210/steam-region-block-bypass // @version 1.16.10 // @description View region-blocked Steam store pages and guest search via anonymous fetch (no account cookies); optional proxy gateway // @description:ru Просмотр заблокированных страниц и гостевой поиск Steam без cookies аккаунта; опциональный proxy gateway // @description:zh-CN 通过无账号 Cookie 查看区域限制页面及访客搜索 Steam 商店;可选代理网关 // @description:es Muestra páginas bloqueadas y búsqueda invitado en Steam sin cookies de cuenta; gateway proxy opcional // @description:pt-BR Mostra páginas bloqueadas e busca convidado na Steam sem cookies da conta; gateway proxy opcional // @description:de Zeigt gesperrte Store-Seiten und Gast-Suche ohne Account-Cookies; optionaler Proxy-Gateway // @description:fr Affiche les pages bloquées et la recherche invité Steam sans cookies de compte; gateway proxy optionnel // @description:ja 地域制限ページとゲスト検索をアカウントCookieなしで表示。任意のプロキシゲートウェイ // @description:ko 지역 제한 페이지와 게스트 검색을 계정 쿠키 없이 표시. 선택적 프록시 게이트웨이 // @description:pl Pokazuje zablokowane strony i wyszukiwanie gościa w Sklepie Steam bez cookies konta; opcjonalny gateway proxy // @author NemoKing1210 // @tag steam // @tag store // @homepageURL https://github.com/NemoKing1210/steam-region-block-bypass // @supportURL https://github.com/NemoKing1210/steam-region-block-bypass/issues // @updateURL https://raw.githubusercontent.com/NemoKing1210/steam-region-block-bypass/main/steam-region-block-bypass.user.js // @downloadURL https://raw.githubusercontent.com/NemoKing1210/steam-region-block-bypass/main/steam-region-block-bypass.user.js // @license MIT // @icon https://store.steampowered.com/favicon.ico // @match https://store.steampowered.com/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect store.steampowered.com // @connect * // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; const REPO_URL = 'https://github.com/NemoKing1210/steam-region-block-bypass'; const STORAGE_KEY = 'srbb_settings'; const CACHE_STORAGE_KEY = 'srbb_page_cache'; const BLOCKED_APPS_STORAGE_KEY = 'srbb_blocked_apps'; /** One-time upgrade: guest search used to default off */ const SEARCH_DEFAULT_ON_FLAG = 'srbb_migrated_search_default_on_v1'; /** Soft cap so GM storage does not grow without bound */ const CACHE_MAX_ENTRIES = 30; const BLOCKED_APPS_MAX_ENTRIES = 500; const PROBE_CONCURRENCY_MAX = 5; /** How many guest suggest rows to fetch/show from /search/results */ const SUGGEST_RESULT_COUNT = 25; /** Upper bound for the settings field (7 days) */ const CACHE_MINUTES_MAX = 10080; const DEFAULT_SETTINGS = { proxyEnabled: false, proxyHost: '', proxyPort: '', proxyUser: '', proxyPass: '', /** gateway | path | query */ proxyMode: 'gateway', countryCode: '', autoBypass: true, // true = replace immediately; false = show button /** Guest HTML TTL in minutes; 0 disables caching */ cacheMinutes: 60, /** Guest search: anonymous suggest dropdown in the header */ searchUnblocked: true, /** Guest /search page: anonymous refetch + inject (opt-in; off by default) */ searchPageUnblocked: false, /** Remember app IDs when Steam shows a region block */ rememberBlockedApps: true, /** Highlight remembered blocked apps in guest search */ markBlockedInSearch: true, /** Probe search results with account cookies to detect region locks */ probeBlockedInSearch: true, /** suggest | search | both */ probeBlockedScope: 'both', /** Parallel account probes (1–5) */ probeBlockedConcurrency: 3, }; const SUPPORTED_LOCALES = ['en', 'ru', 'zh-CN', 'es', 'pt-BR', 'de', 'fr', 'ja', 'ko', 'pl']; /** UI locale → Steam store language cookie / `?l=` value */ const STEAM_LANG_BY_LOCALE = { en: 'english', ru: 'russian', 'zh-CN': 'schinese', es: 'spanish', 'pt-BR': 'brazilian', de: 'german', fr: 'french', ja: 'japanese', ko: 'koreana', pl: 'polish', }; /** Steam_Language → Accept-Language header */ const ACCEPT_LANG_BY_STEAM = { english: 'en-US,en;q=0.9', russian: 'ru-RU,ru;q=0.9,en;q=0.8', schinese: 'zh-CN,zh;q=0.9,en;q=0.8', tchinese: 'zh-TW,zh;q=0.9,en;q=0.8', spanish: 'es-ES,es;q=0.9,en;q=0.8', latam: 'es-419,es;q=0.9,en;q=0.8', brazilian: 'pt-BR,pt;q=0.9,en;q=0.8', portuguese: 'pt-PT,pt;q=0.9,en;q=0.8', german: 'de-DE,de;q=0.9,en;q=0.8', french: 'fr-FR,fr;q=0.9,en;q=0.8', japanese: 'ja-JP,ja;q=0.9,en;q=0.8', koreana: 'ko-KR,ko;q=0.9,en;q=0.8', polish: 'pl-PL,pl;q=0.9,en;q=0.8', italian: 'it-IT,it;q=0.9,en;q=0.8', dutch: 'nl-NL,nl;q=0.9,en;q=0.8', turkish: 'tr-TR,tr;q=0.9,en;q=0.8', thai: 'th-TH,th;q=0.9,en;q=0.8', ukrainian: 'uk-UA,uk;q=0.9,en;q=0.8', vietnamese: 'vi-VN,vi;q=0.9,en;q=0.8', indonesian: 'id-ID,id;q=0.9,en;q=0.8', }; const MESSAGES = { en: { menuSettings: 'Steam Region Bypass — Settings', loading: 'Loading store page without account cookies…', stillBlockedProxy: 'Still blocked. Proxy IP is likely in a restricted region — try another exit node.', stillBlockedNoProxy: 'Still blocked by IP/region. Enable a proxy gateway in settings and point it to an unrestricted region.', ageGate: 'Steam returned an age gate. Retry — birthtime cookies are sent automatically; if it persists, open the app once and confirm age.', noContent: 'Fetched page had no store content. Check proxy settings or try again.', failedLoad: 'Failed to load: {error}', networkError: 'Network error', requestTimeout: 'Request timed out', retry: 'Retry', settings: 'Settings', badge: 'Region Bypass', bannerBlockedBadge: 'Region blocked', bannerTitle: 'This game is unavailable in your region', bannerBody: 'Store page is shown via anonymous guest fetch (no account cookies)', viaProxy: 'via proxy gateway', viaCache: 'from cache', reload: 'Reload', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'Proxy gateway ON', proxyOff: 'Proxy gateway OFF', panelTitle: 'Region Bypass', panelSubtitle: 'Anonymous store fetch · optional proxy gateway', tabGeneral: 'General', tabSearch: 'Search', tabProxy: 'Proxy', close: 'Close', bypassTrigger: 'On blocked pages', bypassTriggerAuto: 'Replace page automatically', bypassTriggerButton: 'Show a button to replace', bypassTriggerHint: 'Automatic mode replaces the Oops page immediately. Button mode keeps the error page and adds a control to start the bypass.', bypassOffer: 'This page is blocked in your region. Load the store page without account cookies?', bypassNow: 'Show store page', storeCountry: 'Store country (cc)', storeCountryHint: 'Optional Steam store country override for guest requests.', cacheMinutes: 'Cache duration (minutes)', cacheMinutesHint: 'How long to reuse a successful guest page before refetching. 0 disables cache. Reload always fetches fresh.', searchUnblocked: 'Guest search suggestions', searchUnblockedHint: 'Header search suggestions are fetched without account cookies (same guest stack as app bypass). On by default.', searchPageUnblocked: 'Guest /search page', searchPageUnblockedHint: 'Replace Steam /search results with an anonymous guest fetch. Off by default — enable if you need region-blocked titles on the full search page.', suggestGuestNotice: 'Region Bypass guest search is on — results load without account cookies and can show region-blocked titles.', suggestGuestSettings: 'Disable in Settings → Search', suggestEmpty: 'Type to search the store as a guest (no account cookies)', suggestLoading: 'Searching…', suggestFailed: 'Search suggestions failed: {error}', suggestNoResults: 'No results', suggestTypeGame: 'Game', suggestTypeApp: 'App', suggestTypeSoftware: 'Software', suggestTypeDlc: 'DLC', suggestTypeBundle: 'Bundle', suggestFree: 'Free', suggestControllerFull: 'Full controller', suggestControllerPartial: 'Partial controller', suggestAppId: 'App ID {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: 'Region blocked', suggestComingSoon: 'Coming soon', suggestStatAvailable: 'Available {count}', suggestStatBlocked: 'Blocked {count}', rememberBlockedApps: 'Remember blocked games', rememberBlockedAppsHint: 'When Steam shows a region error on an app page, save its ID locally for guest-search highlights.', markBlockedInSearch: 'Highlight blocked in search', markBlockedInSearchHint: 'Show a region-blocked badge on saved games in guest search results.', probeBlockedInSearch: 'Auto-detect blocked in search', probeBlockedInSearchHint: 'After guest search renders, open each app page with your Steam cookies. Region-locked games are added to the blocked list. Requires being signed in.', probeBlockedScope: 'Check in', probeBlockedScopeBoth: 'Suggestions and search page', probeBlockedScopeSuggest: 'Suggestions only', probeBlockedScopeSearch: 'Search page only', probeBlockedConcurrency: 'Parallel checks', probeBlockedConcurrencyHint: 'How many app pages to probe at once (lower is gentler on Steam).', probeProgress: 'Checking region locks… {done}/{total}', probeFound: 'Added {count} blocked game(s)', probeNeedLogin: 'Sign in to Steam to auto-detect blocked games.', probeNeedRemember: 'Turn on “Remember blocked games” to save auto-detected apps.', blockedAppsCount: '{count} blocked games saved', viewBlockedApps: 'View list', hideBlockedApps: 'Hide list', blockedAppsEmpty: 'No blocked games saved yet.', blockedAppUntitled: 'App {id}', clearBlockedApps: 'Clear list', searchPageLoading: 'Loading search results without account cookies…', searchPageBanner: 'Guest search via Region Bypass (anonymous fetch)', searchPageBannerSettings: 'Settings', searchPageNoContent: 'No search results in guest response. Check proxy or try again.', useProxy: 'Use proxy gateway', on: 'ON', off: 'OFF', gatewayMode: 'Gateway mode', host: 'Host', port: 'Port', username: 'Username', password: 'Password', optional: 'optional', proxyHint: 'Userscripts cannot set a system SOCKS/HTTP proxy. Point these fields at a local/remote HTTP gateway that fetches URLs from another region (for example a small CORS proxy behind your VPN).', cancel: 'Cancel', save: 'Save', saveReload: 'Save & Reload page', repoLink: 'GitHub', repoAbout: 'Source code, updates, and issue reports', }, ru: { menuSettings: 'Steam Region Bypass — Настройки', loading: 'Загрузка страницы магазина без cookies аккаунта…', stillBlockedProxy: 'Всё ещё заблокировано. IP прокси, вероятно, в ограниченном регионе — попробуйте другой выходной узел.', stillBlockedNoProxy: 'Всё ещё заблокировано по IP/региону. Включите proxy gateway в настройках и направьте его в неограниченный регион.', ageGate: 'Steam вернул проверку возраста. Повторите — cookies birthtime отправляются автоматически; если не поможет, откройте приложение один раз и подтвердите возраст.', noContent: 'В загруженной странице нет контента магазина. Проверьте настройки прокси или попробуйте снова.', failedLoad: 'Не удалось загрузить: {error}', networkError: 'Ошибка сети', requestTimeout: 'Время запроса истекло', retry: 'Повторить', settings: 'Настройки', badge: 'Region Bypass', bannerBlockedBadge: 'Регион заблокирован', bannerTitle: 'Эта игра недоступна в вашем регионе', bannerBody: 'Страница магазина показана через анонимный гостевой запрос (без cookies аккаунта)', viaProxy: 'через proxy gateway', viaCache: 'из кеша', reload: 'Обновить', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'Proxy gateway ВКЛ', proxyOff: 'Proxy gateway ВЫКЛ', panelTitle: 'Region Bypass', panelSubtitle: 'Анонимный запрос магазина · опциональный proxy gateway', tabGeneral: 'Основные', tabSearch: 'Поиск', tabProxy: 'Прокси', close: 'Закрыть', bypassTrigger: 'На заблокированных страницах', bypassTriggerAuto: 'Сразу заменять страницу', bypassTriggerButton: 'Показать кнопку для замены', bypassTriggerHint: 'Автоматический режим сразу заменяет страницу Oops. Режим с кнопкой оставляет ошибку и добавляет кнопку запуска обхода.', bypassOffer: 'Страница недоступна в вашем регионе. Загрузить страницу магазина без cookies аккаунта?', bypassNow: 'Показать страницу магазина', storeCountry: 'Страна магазина (cc)', storeCountryHint: 'Необязательная подмена страны магазина Steam для гостевых запросов.', cacheMinutes: 'Время кеша (минуты)', cacheMinutesHint: 'Как долго повторно использовать успешно загруженную гостевую страницу. 0 отключает кеш. «Обновить» всегда запрашивает заново.', searchUnblocked: 'Гостевые подсказки поиска', searchUnblockedHint: 'Подсказки в шапке запрашиваются без cookies аккаунта (тот же гостевой стек, что и для страниц приложений). Включены по умолчанию.', searchPageUnblocked: 'Гостевая страница /search', searchPageUnblockedHint: 'Подменяет результаты Steam /search анонимным гостевым запросом. Выключено по умолчанию — включите, если нужны регионально заблокированные игры на полной странице поиска.', suggestGuestNotice: 'Включён гостевой поиск Region Bypass — результаты без cookies аккаунта, видны и регионально заблокированные игры.', suggestGuestSettings: 'Отключить: Настройки → Поиск', suggestEmpty: 'Введите запрос для гостевого поиска (без cookies аккаунта)', suggestLoading: 'Поиск…', suggestFailed: 'Не удалось загрузить подсказки: {error}', suggestNoResults: 'Ничего не найдено', suggestTypeGame: 'Игра', suggestTypeApp: 'Приложение', suggestTypeSoftware: 'Софт', suggestTypeDlc: 'DLC', suggestTypeBundle: 'Набор', suggestFree: 'Бесплатно', suggestControllerFull: 'Полная поддержка геймпада', suggestControllerPartial: 'Частичная поддержка геймпада', suggestAppId: 'ID приложения {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: 'Регион заблокирован', suggestComingSoon: 'Скоро', suggestStatAvailable: 'Доступно {count}', suggestStatBlocked: 'Заблокировано {count}', rememberBlockedApps: 'Запоминать заблокированные игры', rememberBlockedAppsHint: 'При ошибке региона на странице приложения сохранять её ID локально для подсветки в гостевом поиске.', markBlockedInSearch: 'Подсвечивать в поиске', markBlockedInSearchHint: 'Показывать метку «регион заблокирован» у сохранённых игр в гостевом поиске.', probeBlockedInSearch: 'Автодетект блокировок в поиске', probeBlockedInSearchHint: 'После отрисовки гостевого поиска открывает страницы приложений с вашими cookies Steam. Игры с региональной блокировкой добавляются в список. Нужен вход в аккаунт.', probeBlockedScope: 'Где проверять', probeBlockedScopeBoth: 'Подсказки и страница поиска', probeBlockedScopeSuggest: 'Только подсказки', probeBlockedScopeSearch: 'Только страница поиска', probeBlockedConcurrency: 'Параллельные проверки', probeBlockedConcurrencyHint: 'Сколько страниц приложений проверять одновременно (меньше — мягче для Steam).', probeProgress: 'Проверка региональных блокировок… {done}/{total}', probeFound: 'Добавлено заблокированных: {count}', probeNeedLogin: 'Войдите в Steam, чтобы автоматически находить заблокированные игры.', probeNeedRemember: 'Включите «Запоминать заблокированные игры», чтобы сохранять найденные.', blockedAppsCount: 'Сохранено заблокированных игр: {count}', viewBlockedApps: 'Посмотреть', hideBlockedApps: 'Скрыть', blockedAppsEmpty: 'Пока нет сохранённых заблокированных игр.', blockedAppUntitled: 'Приложение {id}', clearBlockedApps: 'Очистить список', searchPageLoading: 'Загрузка результатов поиска без cookies аккаунта…', searchPageBanner: 'Гостевой поиск Region Bypass (анонимный запрос)', searchPageBannerSettings: 'Настройки', searchPageNoContent: 'В гостевом ответе нет результатов. Проверьте прокси или попробуйте снова.', useProxy: 'Использовать proxy gateway', on: 'ВКЛ', off: 'ВЫКЛ', gatewayMode: 'Режим gateway', host: 'Хост', port: 'Порт', username: 'Имя пользователя', password: 'Пароль', optional: 'необязательно', proxyHint: 'Userscripts не могут задать системный SOCKS/HTTP-прокси. Укажите локальный или удалённый HTTP-gateway, который загружает URL из другого региона (например, небольшой CORS-прокси за VPN).', cancel: 'Отмена', save: 'Сохранить', saveReload: 'Сохранить и перезагрузить', repoLink: 'GitHub', repoAbout: 'Исходный код, обновления и сообщения об ошибках', }, 'zh-CN': { menuSettings: 'Steam Region Bypass — 设置', loading: '正在以无账号 Cookie 的方式加载商店页面…', stillBlockedProxy: '仍然被封锁。代理 IP 可能位于受限地区 — 请尝试其他出口节点。', stillBlockedNoProxy: '仍被 IP/地区封锁。请在设置中启用代理网关,并将其指向不受限地区。', ageGate: 'Steam 返回了年龄验证。请重试 — birthtime Cookie 会自动发送;若仍出现,请先打开一次应用并确认年龄。', noContent: '获取的页面没有商店内容。请检查代理设置或重试。', failedLoad: '加载失败:{error}', networkError: '网络错误', requestTimeout: '请求超时', retry: '重试', settings: '设置', badge: 'Region Bypass', bannerBlockedBadge: '地区限制', bannerTitle: '此游戏在您所在地区不可用', bannerBody: '商店页面通过匿名访客请求显示(无账号 Cookie)', viaProxy: '经由代理网关', viaCache: '来自缓存', reload: '重新加载', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: '代理网关 开', proxyOff: '代理网关 关', panelTitle: 'Region Bypass', panelSubtitle: '匿名商店请求 · 可选代理网关', tabGeneral: '常规', tabSearch: '搜索', tabProxy: '代理', close: '关闭', bypassTrigger: '在被封锁的页面上', bypassTriggerAuto: '自动替换页面', bypassTriggerButton: '显示替换按钮', bypassTriggerHint: '自动模式会立即替换 Oops 页面。按钮模式保留错误页,并添加用于启动绕过的控件。', bypassOffer: '此页面在您所在地区被封锁。是否以无账号 Cookie 的方式加载商店页面?', bypassNow: '显示商店页面', storeCountry: '商店国家/地区 (cc)', storeCountryHint: '可选:覆盖访客请求的 Steam 商店国家/地区。', cacheMinutes: '缓存时长(分钟)', cacheMinutesHint: '成功的访客页面在重新请求前可复用多久。0 禁用缓存。「重新加载」始终获取最新内容。', searchUnblocked: '访客搜索建议', searchUnblockedHint: '顶栏搜索建议以无账号 Cookie 的方式获取(与应用绕过相同的访客栈)。默认开启。', searchPageUnblocked: '访客 /search 页面', searchPageUnblockedHint: '用匿名访客请求替换 Steam /search 结果。默认关闭 — 若需要在完整搜索页看到地区受限游戏,请开启。', suggestGuestNotice: '已启用 Region Bypass 访客搜索 — 结果不带账号 Cookie,可显示地区受限游戏。', suggestGuestSettings: '在“设置 → 搜索”中关闭', suggestEmpty: '输入关键词以访客身份搜索商店(无账号 Cookie)', suggestLoading: '搜索中…', suggestFailed: '搜索建议加载失败:{error}', suggestNoResults: '无结果', suggestTypeGame: '游戏', suggestTypeApp: '应用', suggestTypeSoftware: '软件', suggestTypeDlc: 'DLC', suggestTypeBundle: '捆绑包', suggestFree: '免费', suggestControllerFull: '完整手柄支持', suggestControllerPartial: '部分手柄支持', suggestAppId: '应用 ID {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: '地区限制', suggestComingSoon: '即将推出', suggestStatAvailable: '可用 {count}', suggestStatBlocked: '受限 {count}', rememberBlockedApps: '记住被封锁的游戏', rememberBlockedAppsHint: '当 Steam 在应用页显示区域错误时,本地保存其 ID,以便在访客搜索中高亮。', markBlockedInSearch: '在搜索中高亮', markBlockedInSearchHint: '在访客搜索结果中为已保存的游戏显示“地区限制”标记。', probeBlockedInSearch: '自动检测搜索中的封锁', probeBlockedInSearchHint: '访客搜索渲染后,用您的 Steam Cookie 打开各应用页。区域受限游戏会加入封锁列表。需要已登录。', probeBlockedScope: '检查范围', probeBlockedScopeBoth: '建议与搜索页', probeBlockedScopeSuggest: '仅建议', probeBlockedScopeSearch: '仅搜索页', probeBlockedConcurrency: '并行检查数', probeBlockedConcurrencyHint: '同时探测的应用页数量(越小对 Steam 越温和)。', probeProgress: '正在检查区域限制… {done}/{total}', probeFound: '已添加 {count} 个封锁游戏', probeNeedLogin: '请登录 Steam 以自动检测封锁游戏。', probeNeedRemember: '请开启“记住被封锁的游戏”以保存自动检测结果。', blockedAppsCount: '已保存 {count} 个被封锁游戏', viewBlockedApps: '查看列表', hideBlockedApps: '隐藏列表', blockedAppsEmpty: '尚未保存被封锁的游戏。', blockedAppUntitled: '应用 {id}', clearBlockedApps: '清空列表', searchPageLoading: '正在以无账号 Cookie 加载搜索结果…', searchPageBanner: '访客搜索(Region Bypass,匿名请求)', searchPageBannerSettings: '设置', searchPageNoContent: '访客响应中没有搜索结果。请检查代理或重试。', useProxy: '使用代理网关', on: '开', off: '关', gatewayMode: '网关模式', host: '主机', port: '端口', username: '用户名', password: '密码', optional: '可选', proxyHint: '用户脚本无法设置系统 SOCKS/HTTP 代理。请将这些字段指向可从其他地区获取 URL 的本地/远程 HTTP 网关(例如位于 VPN 后的小型 CORS 代理)。', cancel: '取消', save: '保存', saveReload: '保存并刷新页面', repoLink: 'GitHub', repoAbout: '源代码、更新与问题反馈', }, es: { menuSettings: 'Steam Region Bypass — Ajustes', loading: 'Cargando la página de la tienda sin cookies de cuenta…', stillBlockedProxy: 'Sigue bloqueado. La IP del proxy probablemente está en una región restringida — prueba otro nodo de salida.', stillBlockedNoProxy: 'Sigue bloqueado por IP/región. Activa un proxy gateway en ajustes y apúntalo a una región sin restricciones.', ageGate: 'Steam devolvió una verificación de edad. Reintenta — las cookies birthtime se envían automáticamente; si persiste, abre la app una vez y confirma la edad.', noContent: 'La página obtenida no tenía contenido de la tienda. Revisa los ajustes del proxy o inténtalo de nuevo.', failedLoad: 'Error al cargar: {error}', networkError: 'Error de red', requestTimeout: 'Tiempo de espera agotado', retry: 'Reintentar', settings: 'Ajustes', badge: 'Region Bypass', bannerBlockedBadge: 'Región bloqueada', bannerTitle: 'Este juego no está disponible en tu región', bannerBody: 'La página de la tienda se muestra mediante una petición anónima de invitado (sin cookies de cuenta)', viaProxy: 'vía proxy gateway', viaCache: 'desde caché', reload: 'Recargar', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'Proxy gateway ON', proxyOff: 'Proxy gateway OFF', panelTitle: 'Region Bypass', panelSubtitle: 'Petición anónima a la tienda · proxy gateway opcional', tabGeneral: 'General', tabSearch: 'Búsqueda', tabProxy: 'Proxy', close: 'Cerrar', bypassTrigger: 'En páginas bloqueadas', bypassTriggerAuto: 'Reemplazar la página automáticamente', bypassTriggerButton: 'Mostrar un botón para reemplazar', bypassTriggerHint: 'El modo automático reemplaza la página Oops de inmediato. El modo botón deja la página de error y añade un control para iniciar el bypass.', bypassOffer: 'Esta página está bloqueada en tu región. ¿Cargar la página de la tienda sin cookies de cuenta?', bypassNow: 'Mostrar página de la tienda', storeCountry: 'País de la tienda (cc)', storeCountryHint: 'Anulación opcional del país de la tienda Steam para peticiones de invitado.', cacheMinutes: 'Duración de caché (minutos)', cacheMinutesHint: 'Cuánto tiempo reutilizar una página de invitado correcta antes de volver a pedirla. 0 desactiva la caché. Recargar siempre obtiene datos frescos.', searchUnblocked: 'Sugerencias de búsqueda invitado', searchUnblockedHint: 'Las sugerencias de la barra se obtienen sin cookies de cuenta (mismo stack invitado que el bypass de apps). Activado por defecto.', searchPageUnblocked: 'Página /search invitado', searchPageUnblockedHint: 'Sustituye los resultados de Steam /search por una petición anónima de invitado. Desactivado por defecto — actívalo si necesitas títulos bloqueados por región en la página de búsqueda completa.', suggestGuestNotice: 'La búsqueda invitado de Region Bypass está activa — resultados sin cookies de cuenta; pueden verse títulos bloqueados por región.', suggestGuestSettings: 'Desactivar en Ajustes → Búsqueda', suggestEmpty: 'Escribe para buscar en la tienda como invitado (sin cookies de cuenta)', suggestLoading: 'Buscando…', suggestFailed: 'Error al cargar sugerencias: {error}', suggestNoResults: 'Sin resultados', suggestTypeGame: 'Juego', suggestTypeApp: 'App', suggestTypeSoftware: 'Software', suggestTypeDlc: 'DLC', suggestTypeBundle: 'Paquete', suggestFree: 'Gratis', suggestControllerFull: 'Mando completo', suggestControllerPartial: 'Mando parcial', suggestAppId: 'ID de app {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: 'Región bloqueada', suggestComingSoon: 'Próximamente', suggestStatAvailable: 'Disponibles {count}', suggestStatBlocked: 'Bloqueados {count}', rememberBlockedApps: 'Recordar juegos bloqueados', rememberBlockedAppsHint: 'Si Steam muestra un error regional en una app, guarda su ID localmente para resaltarla en la búsqueda invitado.', markBlockedInSearch: 'Resaltar en búsqueda', markBlockedInSearchHint: 'Muestra una insignia de región bloqueada en juegos guardados en la búsqueda invitado.', probeBlockedInSearch: 'Detectar bloqueados en la búsqueda', probeBlockedInSearchHint: 'Tras renderizar la búsqueda invitado, abre cada app con tus cookies de Steam. Las bloqueadas por región se añaden a la lista. Requiere iniciar sesión.', probeBlockedScope: 'Comprobar en', probeBlockedScopeBoth: 'Sugerencias y página de búsqueda', probeBlockedScopeSuggest: 'Solo sugerencias', probeBlockedScopeSearch: 'Solo página de búsqueda', probeBlockedConcurrency: 'Comprobaciones en paralelo', probeBlockedConcurrencyHint: 'Cuántas páginas de app sondear a la vez (menos es más suave con Steam).', probeProgress: 'Comprobando bloqueos regionales… {done}/{total}', probeFound: 'Añadidos {count} juego(s) bloqueado(s)', probeNeedLogin: 'Inicia sesión en Steam para detectar juegos bloqueados automáticamente.', probeNeedRemember: 'Activa «Recordar juegos bloqueados» para guardar los detectados.', blockedAppsCount: '{count} juegos bloqueados guardados', viewBlockedApps: 'Ver lista', hideBlockedApps: 'Ocultar lista', blockedAppsEmpty: 'Aún no hay juegos bloqueados guardados.', blockedAppUntitled: 'App {id}', clearBlockedApps: 'Borrar lista', searchPageLoading: 'Cargando resultados de búsqueda sin cookies de cuenta…', searchPageBanner: 'Búsqueda invitado de Region Bypass (petición anónima)', searchPageBannerSettings: 'Ajustes', searchPageNoContent: 'No hay resultados en la respuesta invitado. Revisa el proxy o inténtalo de nuevo.', useProxy: 'Usar proxy gateway', on: 'ON', off: 'OFF', gatewayMode: 'Modo gateway', host: 'Host', port: 'Puerto', username: 'Usuario', password: 'Contraseña', optional: 'opcional', proxyHint: 'Los userscripts no pueden configurar un proxy SOCKS/HTTP del sistema. Apunta estos campos a un gateway HTTP local/remoto que obtenga URLs desde otra región (por ejemplo un pequeño proxy CORS detrás de tu VPN).', cancel: 'Cancelar', save: 'Guardar', saveReload: 'Guardar y recargar', repoLink: 'GitHub', repoAbout: 'Código fuente, actualizaciones e informes de errores', }, 'pt-BR': { menuSettings: 'Steam Region Bypass — Configurações', loading: 'Carregando a página da loja sem cookies da conta…', stillBlockedProxy: 'Ainda bloqueado. O IP do proxy provavelmente está em uma região restrita — tente outro nó de saída.', stillBlockedNoProxy: 'Ainda bloqueado por IP/região. Ative um proxy gateway nas configurações e aponte para uma região sem restrições.', ageGate: 'A Steam retornou uma verificação de idade. Tente de novo — cookies birthtime são enviados automaticamente; se persistir, abra o app uma vez e confirme a idade.', noContent: 'A página obtida não tinha conteúdo da loja. Verifique as configurações do proxy ou tente novamente.', failedLoad: 'Falha ao carregar: {error}', networkError: 'Erro de rede', requestTimeout: 'Tempo esgotado', retry: 'Tentar de novo', settings: 'Configurações', badge: 'Region Bypass', bannerBlockedBadge: 'Região bloqueada', bannerTitle: 'Este jogo não está disponível na sua região', bannerBody: 'A página da loja é exibida via requisição anônima de convidado (sem cookies da conta)', viaProxy: 'via proxy gateway', viaCache: 'do cache', reload: 'Recarregar', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'Proxy gateway ON', proxyOff: 'Proxy gateway OFF', panelTitle: 'Region Bypass', panelSubtitle: 'Requisição anônima à loja · proxy gateway opcional', tabGeneral: 'Geral', tabSearch: 'Busca', tabProxy: 'Proxy', close: 'Fechar', bypassTrigger: 'Em páginas bloqueadas', bypassTriggerAuto: 'Substituir a página automaticamente', bypassTriggerButton: 'Mostrar um botão para substituir', bypassTriggerHint: 'O modo automático substitui a página Oops imediatamente. O modo botão mantém a página de erro e adiciona um controle para iniciar o bypass.', bypassOffer: 'Esta página está bloqueada na sua região. Carregar a página da loja sem cookies da conta?', bypassNow: 'Mostrar página da loja', storeCountry: 'País da loja (cc)', storeCountryHint: 'Substituição opcional do país da loja Steam para requisições de convidado.', cacheMinutes: 'Duração do cache (minutos)', cacheMinutesHint: 'Por quanto tempo reutilizar uma página de convidado bem-sucedida antes de buscar de novo. 0 desativa o cache. Recarregar sempre busca dados novos.', searchUnblocked: 'Sugestões de busca convidado', searchUnblockedHint: 'Sugestões da barra são obtidas sem cookies da conta (mesmo stack convidado do bypass de apps). Ativado por padrão.', searchPageUnblocked: 'Página /search convidado', searchPageUnblockedHint: 'Substitui os resultados do Steam /search por uma requisição anônima de convidado. Desativado por padrão — ative se precisar de títulos bloqueados por região na página de busca completa.', suggestGuestNotice: 'Busca convidado do Region Bypass ativa — resultados sem cookies da conta; títulos bloqueados por região podem aparecer.', suggestGuestSettings: 'Desativar em Configurações → Busca', suggestEmpty: 'Digite para buscar na loja como convidado (sem cookies da conta)', suggestLoading: 'Buscando…', suggestFailed: 'Falha ao carregar sugestões: {error}', suggestNoResults: 'Sem resultados', suggestTypeGame: 'Jogo', suggestTypeApp: 'App', suggestTypeSoftware: 'Software', suggestTypeDlc: 'DLC', suggestTypeBundle: 'Pacote', suggestFree: 'Grátis', suggestControllerFull: 'Controle total', suggestControllerPartial: 'Controle parcial', suggestAppId: 'ID do app {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: 'Região bloqueada', suggestComingSoon: 'Em breve', suggestStatAvailable: 'Disponíveis {count}', suggestStatBlocked: 'Bloqueados {count}', rememberBlockedApps: 'Lembrar jogos bloqueados', rememberBlockedAppsHint: 'Quando a Steam mostrar erro regional numa app, salva o ID localmente para destacar na busca convidado.', markBlockedInSearch: 'Destacar na busca', markBlockedInSearchHint: 'Mostra um selo de região bloqueada nos jogos salvos na busca convidado.', probeBlockedInSearch: 'Detectar bloqueados na busca', probeBlockedInSearchHint: 'Após renderizar a busca convidado, abre cada app com seus cookies da Steam. Jogos bloqueados por região entram na lista. É preciso estar logado.', probeBlockedScope: 'Verificar em', probeBlockedScopeBoth: 'Sugestões e página de busca', probeBlockedScopeSuggest: 'Somente sugestões', probeBlockedScopeSearch: 'Somente página de busca', probeBlockedConcurrency: 'Verificações em paralelo', probeBlockedConcurrencyHint: 'Quantas páginas de app sondar ao mesmo tempo (menor = mais suave com a Steam).', probeProgress: 'Verificando bloqueios regionais… {done}/{total}', probeFound: 'Adicionado(s) {count} jogo(s) bloqueado(s)', probeNeedLogin: 'Entre na Steam para detectar jogos bloqueados automaticamente.', probeNeedRemember: 'Ative «Lembrar jogos bloqueados» para salvar os detectados.', blockedAppsCount: '{count} jogos bloqueados salvos', viewBlockedApps: 'Ver lista', hideBlockedApps: 'Ocultar lista', blockedAppsEmpty: 'Nenhum jogo bloqueado salvo ainda.', blockedAppUntitled: 'App {id}', clearBlockedApps: 'Limpar lista', searchPageLoading: 'Carregando resultados da busca sem cookies da conta…', searchPageBanner: 'Busca convidado do Region Bypass (requisição anônima)', searchPageBannerSettings: 'Configurações', searchPageNoContent: 'Sem resultados na resposta convidado. Verifique o proxy ou tente novamente.', useProxy: 'Usar proxy gateway', on: 'ON', off: 'OFF', gatewayMode: 'Modo gateway', host: 'Host', port: 'Porta', username: 'Usuário', password: 'Senha', optional: 'opcional', proxyHint: 'Userscripts não podem definir um proxy SOCKS/HTTP do sistema. Aponte estes campos para um gateway HTTP local/remoto que busque URLs de outra região (por exemplo um pequeno proxy CORS atrás da sua VPN).', cancel: 'Cancelar', save: 'Salvar', saveReload: 'Salvar e recarregar', repoLink: 'GitHub', repoAbout: 'Código-fonte, atualizações e relatos de problemas', }, de: { menuSettings: 'Steam Region Bypass — Einstellungen', loading: 'Store-Seite wird ohne Account-Cookies geladen…', stillBlockedProxy: 'Immer noch gesperrt. Die Proxy-IP liegt wahrscheinlich in einer eingeschränkten Region — anderen Exit-Node versuchen.', stillBlockedNoProxy: 'Immer noch per IP/Region gesperrt. Proxy-Gateway in den Einstellungen aktivieren und auf eine freie Region ausrichten.', ageGate: 'Steam hat eine Altersfreigabe zurückgegeben. Erneut versuchen — birthtime-Cookies werden automatisch gesendet; falls es bleibt, App einmal öffnen und Alter bestätigen.', noContent: 'Abgerufene Seite hatte keinen Store-Inhalt. Proxy-Einstellungen prüfen oder erneut versuchen.', failedLoad: 'Laden fehlgeschlagen: {error}', networkError: 'Netzwerkfehler', requestTimeout: 'Zeitüberschreitung', retry: 'Erneut', settings: 'Einstellungen', badge: 'Region Bypass', bannerBlockedBadge: 'Region gesperrt', bannerTitle: 'Dieses Spiel ist in deiner Region nicht verfügbar', bannerBody: 'Die Store-Seite wird über einen anonymen Gastabruf angezeigt (ohne Account-Cookies)', viaProxy: 'über Proxy-Gateway', viaCache: 'aus dem Cache', reload: 'Neu laden', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'Proxy-Gateway AN', proxyOff: 'Proxy-Gateway AUS', panelTitle: 'Region Bypass', panelSubtitle: 'Anonymer Store-Abruf · optionaler Proxy-Gateway', tabGeneral: 'Allgemein', tabSearch: 'Suche', tabProxy: 'Proxy', close: 'Schließen', bypassTrigger: 'Auf gesperrten Seiten', bypassTriggerAuto: 'Seite automatisch ersetzen', bypassTriggerButton: 'Schaltfläche zum Ersetzen anzeigen', bypassTriggerHint: 'Der Automatikmodus ersetzt die Oops-Seite sofort. Der Schaltflächenmodus lässt die Fehlerseite und fügt eine Steuerung zum Starten des Bypasses hinzu.', bypassOffer: 'Diese Seite ist in deiner Region gesperrt. Store-Seite ohne Account-Cookies laden?', bypassNow: 'Store-Seite anzeigen', storeCountry: 'Store-Land (cc)', storeCountryHint: 'Optionale Überschreibung des Steam-Store-Lands für Gastanfragen.', cacheMinutes: 'Cache-Dauer (Minuten)', cacheMinutesHint: 'Wie lange eine erfolgreiche Gastseite wiederverwendet wird, bevor neu geladen wird. 0 deaktiviert den Cache. Neu laden holt immer frische Daten.', searchUnblocked: 'Gast-Suchvorschläge', searchUnblockedHint: 'Suchvorschläge in der Kopfzeile werden ohne Account-Cookies abgerufen (gleicher Gast-Stack wie App-Bypass). Standardmäßig aktiv.', searchPageUnblocked: 'Gast-/search-Seite', searchPageUnblockedHint: 'Ersetzt Steam-/search-Ergebnisse durch einen anonymen Gastabruf. Standardmäßig aus — aktivieren, wenn regional gesperrte Titel auf der vollständigen Suchseite nötig sind.', suggestGuestNotice: 'Region-Bypass-Gast-Suche ist aktiv — Ergebnisse ohne Account-Cookies; regional gesperrte Titel können erscheinen.', suggestGuestSettings: 'Deaktivieren unter Einstellungen → Suche', suggestEmpty: 'Tippen, um den Store als Gast zu durchsuchen (ohne Account-Cookies)', suggestLoading: 'Suche…', suggestFailed: 'Suchvorschläge fehlgeschlagen: {error}', suggestNoResults: 'Keine Ergebnisse', suggestTypeGame: 'Spiel', suggestTypeApp: 'App', suggestTypeSoftware: 'Software', suggestTypeDlc: 'DLC', suggestTypeBundle: 'Bundle', suggestFree: 'Kostenlos', suggestControllerFull: 'Volle Controller-Unterstützung', suggestControllerPartial: 'Teilweise Controller-Unterstützung', suggestAppId: 'App-ID {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: 'Region gesperrt', suggestComingSoon: 'Demnächst', suggestStatAvailable: 'Verfügbar {count}', suggestStatBlocked: 'Gesperrt {count}', rememberBlockedApps: 'Gesperrte Spiele merken', rememberBlockedAppsHint: 'Wenn Steam auf einer App-Seite einen Regionsfehler zeigt, ID lokal speichern für Hervorhebung in der Gast-Suche.', markBlockedInSearch: 'In Suche hervorheben', markBlockedInSearchHint: 'Zeigt ein Regions-gesperrt-Abzeichen für gespeicherte Spiele in der Gast-Suche.', probeBlockedInSearch: 'Gesperrte in Suche erkennen', probeBlockedInSearchHint: 'Nach der Gast-Suche werden App-Seiten mit Ihren Steam-Cookies geladen. Regional gesperrte Spiele kommen auf die Liste. Anmeldung erforderlich.', probeBlockedScope: 'Prüfen in', probeBlockedScopeBoth: 'Vorschläge und Suchseite', probeBlockedScopeSuggest: 'Nur Vorschläge', probeBlockedScopeSearch: 'Nur Suchseite', probeBlockedConcurrency: 'Parallele Prüfungen', probeBlockedConcurrencyHint: 'Wie viele App-Seiten gleichzeitig geprüft werden (niedriger = schonender für Steam).', probeProgress: 'Regionsperren werden geprüft… {done}/{total}', probeFound: '{count} gesperrte(s) Spiel(e) hinzugefügt', probeNeedLogin: 'Melden Sie sich bei Steam an, um gesperrte Spiele automatisch zu erkennen.', probeNeedRemember: 'Aktivieren Sie „Gesperrte Spiele merken“, um erkannte Apps zu speichern.', blockedAppsCount: '{count} gesperrte Spiele gespeichert', viewBlockedApps: 'Liste anzeigen', hideBlockedApps: 'Liste ausblenden', blockedAppsEmpty: 'Noch keine gesperrten Spiele gespeichert.', blockedAppUntitled: 'App {id}', clearBlockedApps: 'Liste leeren', searchPageLoading: 'Suchergebnisse werden ohne Account-Cookies geladen…', searchPageBanner: 'Gast-Suche von Region Bypass (anonymer Abruf)', searchPageBannerSettings: 'Einstellungen', searchPageNoContent: 'Keine Suchergebnisse in der Gastantwort. Proxy prüfen oder erneut versuchen.', useProxy: 'Proxy-Gateway verwenden', on: 'AN', off: 'AUS', gatewayMode: 'Gateway-Modus', host: 'Host', port: 'Port', username: 'Benutzername', password: 'Passwort', optional: 'optional', proxyHint: 'Userscripts können keinen System-SOCKS/HTTP-Proxy setzen. Richte diese Felder auf ein lokales/remotetes HTTP-Gateway, das URLs aus einer anderen Region abruft (z. B. kleiner CORS-Proxy hinter deinem VPN).', cancel: 'Abbrechen', save: 'Speichern', saveReload: 'Speichern & Seite neu laden', repoLink: 'GitHub', repoAbout: 'Quellcode, Updates und Fehlerberichte', }, fr: { menuSettings: 'Steam Region Bypass — Paramètres', loading: 'Chargement de la page boutique sans cookies de compte…', stillBlockedProxy: "Toujours bloqué. L'IP du proxy est probablement dans une région restreinte — essayez un autre nœud de sortie.", stillBlockedNoProxy: 'Toujours bloqué par IP/région. Activez un proxy gateway dans les paramètres et pointez-le vers une région non restreinte.', ageGate: "Steam a renvoyé une vérification d'âge. Réessayez — les cookies birthtime sont envoyés automatiquement ; si cela persiste, ouvrez l'app une fois et confirmez l'âge.", noContent: "La page récupérée n'avait pas de contenu boutique. Vérifiez les paramètres du proxy ou réessayez.", failedLoad: 'Échec du chargement : {error}', networkError: 'Erreur réseau', requestTimeout: 'Délai dépassé', retry: 'Réessayer', settings: 'Paramètres', badge: 'Region Bypass', bannerBlockedBadge: 'Région bloquée', bannerTitle: "Ce jeu n'est pas disponible dans votre région", bannerBody: 'La page boutique est affichée via une requête anonyme invité (sans cookies de compte)', viaProxy: 'via proxy gateway', viaCache: 'depuis le cache', reload: 'Recharger', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'Proxy gateway ON', proxyOff: 'Proxy gateway OFF', panelTitle: 'Region Bypass', panelSubtitle: 'Requête boutique anonyme · proxy gateway optionnel', tabGeneral: 'Général', tabSearch: 'Recherche', tabProxy: 'Proxy', close: 'Fermer', bypassTrigger: 'Sur les pages bloquées', bypassTriggerAuto: 'Remplacer la page automatiquement', bypassTriggerButton: 'Afficher un bouton pour remplacer', bypassTriggerHint: 'Le mode automatique remplace immédiatement la page Oops. Le mode bouton conserve la page d’erreur et ajoute un contrôle pour lancer le contournement.', bypassOffer: 'Cette page est bloquée dans votre région. Charger la page boutique sans cookies de compte ?', bypassNow: 'Afficher la page boutique', storeCountry: 'Pays de la boutique (cc)', storeCountryHint: 'Remplacement optionnel du pays de la boutique Steam pour les requêtes invité.', cacheMinutes: 'Durée du cache (minutes)', cacheMinutesHint: 'Combien de temps réutiliser une page invité réussie avant de la redemander. 0 désactive le cache. Recharger récupère toujours des données fraîches.', searchUnblocked: 'Suggestions de recherche invité', searchUnblockedHint: 'Les suggestions de la barre sont récupérées sans cookies de compte (même pile invité que le bypass d’apps). Activé par défaut.', searchPageUnblocked: 'Page /search invité', searchPageUnblockedHint: 'Remplace les résultats Steam /search par une requête anonyme invité. Désactivé par défaut — activez si vous avez besoin des titres bloqués par région sur la page de recherche complète.', suggestGuestNotice: 'Recherche invité Region Bypass active — résultats sans cookies de compte ; les titres bloqués par région peuvent apparaître.', suggestGuestSettings: 'Désactiver dans Paramètres → Recherche', suggestEmpty: 'Tapez pour chercher dans la boutique en invité (sans cookies de compte)', suggestLoading: 'Recherche…', suggestFailed: 'Échec des suggestions : {error}', suggestNoResults: 'Aucun résultat', suggestTypeGame: 'Jeu', suggestTypeApp: 'App', suggestTypeSoftware: 'Logiciel', suggestTypeDlc: 'DLC', suggestTypeBundle: 'Lot', suggestFree: 'Gratuit', suggestControllerFull: 'Manette complète', suggestControllerPartial: 'Manette partielle', suggestAppId: 'ID app {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: 'Région bloquée', suggestComingSoon: 'Bientôt disponible', suggestStatAvailable: 'Disponibles {count}', suggestStatBlocked: 'Bloqués {count}', rememberBlockedApps: 'Mémoriser les jeux bloqués', rememberBlockedAppsHint: 'Quand Steam affiche une erreur régionale sur une app, enregistrer son ID localement pour la recherche invité.', markBlockedInSearch: 'Surligner dans la recherche', markBlockedInSearchHint: 'Affiche un badge région bloquée sur les jeux enregistrés dans la recherche invité.', probeBlockedInSearch: 'Détecter les bloqués dans la recherche', probeBlockedInSearchHint: 'Après l’affichage de la recherche invité, ouvre chaque page d’app avec vos cookies Steam. Les jeux bloqués par région sont ajoutés à la liste. Connexion requise.', probeBlockedScope: 'Vérifier dans', probeBlockedScopeBoth: 'Suggestions et page de recherche', probeBlockedScopeSuggest: 'Suggestions uniquement', probeBlockedScopeSearch: 'Page de recherche uniquement', probeBlockedConcurrency: 'Vérifications parallèles', probeBlockedConcurrencyHint: 'Nombre de pages d’app sondées en même temps (plus bas = plus doux pour Steam).', probeProgress: 'Vérification des blocages régionaux… {done}/{total}', probeFound: '{count} jeu(x) bloqué(s) ajouté(s)', probeNeedLogin: 'Connectez-vous à Steam pour détecter automatiquement les jeux bloqués.', probeNeedRemember: 'Activez « Mémoriser les jeux bloqués » pour enregistrer les détections.', blockedAppsCount: '{count} jeux bloqués enregistrés', viewBlockedApps: 'Voir la liste', hideBlockedApps: 'Masquer la liste', blockedAppsEmpty: 'Aucun jeu bloqué enregistré pour le moment.', blockedAppUntitled: 'App {id}', clearBlockedApps: 'Vider la liste', searchPageLoading: 'Chargement des résultats sans cookies de compte…', searchPageBanner: 'Recherche invité Region Bypass (requête anonyme)', searchPageBannerSettings: 'Paramètres', searchPageNoContent: 'Aucun résultat dans la réponse invité. Vérifiez le proxy ou réessayez.', useProxy: 'Utiliser le proxy gateway', on: 'ON', off: 'OFF', gatewayMode: 'Mode gateway', host: 'Hôte', port: 'Port', username: "Nom d'utilisateur", password: 'Mot de passe', optional: 'facultatif', proxyHint: 'Les userscripts ne peuvent pas définir un proxy SOCKS/HTTP système. Pointez ces champs vers une passerelle HTTP locale/distante qui récupère les URL depuis une autre région (par ex. un petit proxy CORS derrière votre VPN).', cancel: 'Annuler', save: 'Enregistrer', saveReload: 'Enregistrer et recharger', repoLink: 'GitHub', repoAbout: 'Code source, mises à jour et signalements', }, ja: { menuSettings: 'Steam Region Bypass — 設定', loading: 'アカウントCookieなしでストアページを読み込み中…', stillBlockedProxy: 'まだブロックされています。プロキシIPが制限地域にある可能性があります — 別の出口ノードを試してください。', stillBlockedNoProxy: 'まだIP/地域でブロックされています。設定でプロキシゲートウェイを有効にし、制限のない地域に向けてください。', ageGate: 'Steamが年齢確認を返しました。再試行してください — birthtime Cookieは自動送信されます。続く場合はアプリを一度開いて年齢を確認してください。', noContent: '取得したページにストア内容がありません。プロキシ設定を確認するか、再試行してください。', failedLoad: '読み込みに失敗: {error}', networkError: 'ネットワークエラー', requestTimeout: 'リクエストがタイムアウトしました', retry: '再試行', settings: '設定', badge: 'Region Bypass', bannerBlockedBadge: '地域制限', bannerTitle: 'このゲームはお住まいの地域では利用できません', bannerBody: 'ストアページは匿名ゲスト取得で表示されています(アカウントCookieなし)', viaProxy: 'プロキシゲートウェイ経由', viaCache: 'キャッシュから', reload: '再読み込み', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'プロキシゲートウェイ ON', proxyOff: 'プロキシゲートウェイ OFF', panelTitle: 'Region Bypass', panelSubtitle: '匿名ストアリクエスト · 任意のプロキシゲートウェイ', tabGeneral: '一般', tabSearch: '検索', tabProxy: 'プロキシ', close: '閉じる', bypassTrigger: 'ブロックされたページで', bypassTriggerAuto: 'ページを自動的に置き換える', bypassTriggerButton: '置き換えボタンを表示', bypassTriggerHint: '自動モードはOopsページをすぐに置き換えます。ボタンモードはエラーページを残し、バイパス開始用のコントロールを追加します。', bypassOffer: 'このページはお住まいの地域でブロックされています。アカウントCookieなしでストアページを読み込みますか?', bypassNow: 'ストアページを表示', storeCountry: 'ストアの国 (cc)', storeCountryHint: 'ゲストリクエスト用のSteamストア国の任意上書き。', cacheMinutes: 'キャッシュ時間(分)', cacheMinutesHint: '成功したゲストページを再取得するまでの保持時間。0でキャッシュ無効。「再読み込み」は常に最新を取得します。', searchUnblocked: 'ゲスト検索候補', searchUnblockedHint: 'ヘッダーの検索候補をアカウントCookieなしで取得します(アプリバイパスと同じゲストスタック)。デフォルトでオン。', searchPageUnblocked: 'ゲスト /search ページ', searchPageUnblockedHint: 'Steam の /search 結果を匿名ゲスト取得で置き換えます。デフォルトはオフ — 検索ページ全体で地域制限タイトルが必要な場合にオンにしてください。', suggestGuestNotice: 'Region Bypass のゲスト検索が有効です — アカウントCookieなしで取得し、地域制限タイトルも表示できます。', suggestGuestSettings: '設定 → 検索 で無効化', suggestEmpty: 'ゲストとしてストアを検索するには入力してください(アカウントCookieなし)', suggestLoading: '検索中…', suggestFailed: '検索候補の取得に失敗: {error}', suggestNoResults: '結果なし', suggestTypeGame: 'ゲーム', suggestTypeApp: 'アプリ', suggestTypeSoftware: 'ソフトウェア', suggestTypeDlc: 'DLC', suggestTypeBundle: 'バンドル', suggestFree: '無料', suggestControllerFull: 'フルコントローラー対応', suggestControllerPartial: '一部コントローラー対応', suggestAppId: 'アプリ ID {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: '地域制限', suggestComingSoon: '近日登場', suggestStatAvailable: '利用可 {count}', suggestStatBlocked: '制限 {count}', rememberBlockedApps: 'ブロックされたゲームを記憶', rememberBlockedAppsHint: 'アプリページで地域エラーが出たとき、IDをローカル保存しゲスト検索で強調表示します。', markBlockedInSearch: '検索で強調', markBlockedInSearchHint: 'ゲスト検索で保存済みゲームに地域制限バッジを表示します。', probeBlockedInSearch: '検索でブロックを自動検出', probeBlockedInSearchHint: 'ゲスト検索の描画後、Steam Cookie で各アプリページを開きます。地域制限のゲームをリストに追加します。ログインが必要です。', probeBlockedScope: 'チェック対象', probeBlockedScopeBoth: '候補と検索ページ', probeBlockedScopeSuggest: '候補のみ', probeBlockedScopeSearch: '検索ページのみ', probeBlockedConcurrency: '並列チェック数', probeBlockedConcurrencyHint: '同時に調べるアプリページ数(小さいほど Steam に優しい)。', probeProgress: '地域制限を確認中… {done}/{total}', probeFound: 'ブロックゲームを {count} 件追加', probeNeedLogin: 'ブロックゲームを自動検出するには Steam にログインしてください。', probeNeedRemember: '検出結果を保存するには「ブロックされたゲームを記憶」をオンにしてください。', blockedAppsCount: '保存済みブロックゲーム: {count}', viewBlockedApps: '一覧を表示', hideBlockedApps: '一覧を隠す', blockedAppsEmpty: '保存されたブロックゲームはまだありません。', blockedAppUntitled: 'アプリ {id}', clearBlockedApps: 'リストを消去', searchPageLoading: 'アカウントCookieなしで検索結果を読み込み中…', searchPageBanner: 'Region Bypass ゲスト検索(匿名取得)', searchPageBannerSettings: '設定', searchPageNoContent: 'ゲスト応答に検索結果がありません。プロキシを確認するか再試行してください。', useProxy: 'プロキシゲートウェイを使用', on: 'ON', off: 'OFF', gatewayMode: 'ゲートウェイモード', host: 'ホスト', port: 'ポート', username: 'ユーザー名', password: 'パスワード', optional: '任意', proxyHint: 'ユーザースクリプトはシステムのSOCKS/HTTPプロキシを設定できません。別地域からURLを取得するローカル/リモートHTTPゲートウェイを指定してください(例: VPN背後の小さなCORSプロキシ)。', cancel: 'キャンセル', save: '保存', saveReload: '保存して再読み込み', repoLink: 'GitHub', repoAbout: 'ソースコード・更新・不具合報告', }, ko: { menuSettings: 'Steam Region Bypass — 설정', loading: '계정 쿠키 없이 스토어 페이지를 불러오는 중…', stillBlockedProxy: '여전히 차단됨. 프록시 IP가 제한 지역에 있을 수 있습니다 — 다른 출구 노드를 시도하세요.', stillBlockedNoProxy: '여전히 IP/지역으로 차단됨. 설정에서 프록시 게이트웨이를 켜고 제한 없는 지역으로 연결하세요.', ageGate: 'Steam이 연령 확인을 반환했습니다. 다시 시도하세요 — birthtime 쿠키는 자동으로 전송됩니다. 계속되면 앱을 한 번 열고 나이를 확인하세요.', noContent: '가져온 페이지에 스토어 콘텐츠가 없습니다. 프록시 설정을 확인하거나 다시 시도하세요.', failedLoad: '불러오기 실패: {error}', networkError: '네트워크 오류', requestTimeout: '요청 시간 초과', retry: '다시 시도', settings: '설정', badge: 'Region Bypass', bannerBlockedBadge: '지역 제한', bannerTitle: '이 게임은 해당 지역에서 이용할 수 없습니다', bannerBody: '스토어 페이지는 익명 게스트 요청으로 표시됩니다 (계정 쿠키 없음)', viaProxy: '프록시 게이트웨이 경유', viaCache: '캐시에서', reload: '다시 불러오기', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: '프록시 게이트웨이 ON', proxyOff: '프록시 게이트웨이 OFF', panelTitle: 'Region Bypass', panelSubtitle: '익명 스토어 요청 · 선택적 프록시 게이트웨이', tabGeneral: '일반', tabSearch: '검색', tabProxy: '프록시', close: '닫기', bypassTrigger: '차단된 페이지에서', bypassTriggerAuto: '페이지 자동 교체', bypassTriggerButton: '교체 버튼 표시', bypassTriggerHint: '자동 모드는 Oops 페이지를 즉시 교체합니다. 버튼 모드는 오류 페이지를 유지하고 우회를 시작할 컨트롤을 추가합니다.', bypassOffer: '이 페이지는 해당 지역에서 차단되어 있습니다. 계정 쿠키 없이 스토어 페이지를 불러올까요?', bypassNow: '스토어 페이지 표시', storeCountry: '스토어 국가 (cc)', storeCountryHint: '게스트 요청용 Steam 스토어 국가 선택적 재정의.', cacheMinutes: '캐시 유지 시간(분)', cacheMinutesHint: '성공한 게스트 페이지를 다시 요청하기 전까지 얼마나 재사용할지. 0이면 캐시 비활성. 「다시 불러오기」는 항상 새로 가져옵니다.', searchUnblocked: '게스트 검색 제안', searchUnblockedHint: '헤더 검색 제안을 계정 쿠키 없이 가져옵니다(앱 우회와 동일한 게스트 스택). 기본으로 켜짐.', searchPageUnblocked: '게스트 /search 페이지', searchPageUnblockedHint: 'Steam /search 결과를 익명 게스트 요청으로 바꿉니다. 기본은 꺼짐 — 전체 검색 페이지에서 지역 제한 타이틀이 필요하면 켜세요.', suggestGuestNotice: 'Region Bypass 게스트 검색이 켜져 있습니다 — 계정 쿠키 없이 결과를 가져오며 지역 제한 타이틀도 보일 수 있습니다.', suggestGuestSettings: '설정 → 검색에서 끄기', suggestEmpty: '게스트로 스토어를 검색하려면 입력하세요(계정 쿠키 없음)', suggestLoading: '검색 중…', suggestFailed: '검색 제안을 불러오지 못했습니다: {error}', suggestNoResults: '결과 없음', suggestTypeGame: '게임', suggestTypeApp: '앱', suggestTypeSoftware: '소프트웨어', suggestTypeDlc: 'DLC', suggestTypeBundle: '번들', suggestFree: '무료', suggestControllerFull: '전체 컨트롤러 지원', suggestControllerPartial: '부분 컨트롤러 지원', suggestAppId: '앱 ID {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: '지역 제한', suggestComingSoon: '출시 예정', suggestStatAvailable: '이용 가능 {count}', suggestStatBlocked: '차단됨 {count}', rememberBlockedApps: '차단된 게임 기억', rememberBlockedAppsHint: '앱 페이지에서 지역 오류가 나오면 ID를 로컬에 저장해 게스트 검색에서 강조합니다.', markBlockedInSearch: '검색에서 강조', markBlockedInSearchHint: '게스트 검색에서 저장된 게임에 지역 제한 배지를 표시합니다.', probeBlockedInSearch: '검색에서 차단 자동 감지', probeBlockedInSearchHint: '게스트 검색 렌더 후 Steam 쿠키로 각 앱 페이지를 엽니다. 지역 제한 게임을 목록에 추가합니다. 로그인이 필요합니다.', probeBlockedScope: '확인 범위', probeBlockedScopeBoth: '제안 및 검색 페이지', probeBlockedScopeSuggest: '제안만', probeBlockedScopeSearch: '검색 페이지만', probeBlockedConcurrency: '병렬 확인 수', probeBlockedConcurrencyHint: '동시에 검사할 앱 페이지 수(낮을수록 Steam에 부담이 적음).', probeProgress: '지역 제한 확인 중… {done}/{total}', probeFound: '차단 게임 {count}개 추가됨', probeNeedLogin: '차단 게임을 자동 감지하려면 Steam에 로그인하세요.', probeNeedRemember: '감지 결과를 저장하려면 «차단된 게임 기억»을 켜세요.', blockedAppsCount: '저장된 차단 게임 {count}개', viewBlockedApps: '목록 보기', hideBlockedApps: '목록 숨기기', blockedAppsEmpty: '저장된 차단 게임이 아직 없습니다.', blockedAppUntitled: '앱 {id}', clearBlockedApps: '목록 지우기', searchPageLoading: '계정 쿠키 없이 검색 결과를 불러오는 중…', searchPageBanner: 'Region Bypass 게스트 검색(익명 요청)', searchPageBannerSettings: '설정', searchPageNoContent: '게스트 응답에 검색 결과가 없습니다. 프록시를 확인하거나 다시 시도하세요.', useProxy: '프록시 게이트웨이 사용', on: 'ON', off: 'OFF', gatewayMode: '게이트웨이 모드', host: '호스트', port: '포트', username: '사용자 이름', password: '비밀번호', optional: '선택', proxyHint: '유저스크립트는 시스템 SOCKS/HTTP 프록시를 설정할 수 없습니다. 다른 지역에서 URL을 가져오는 로컬/원격 HTTP 게이트웨이를 지정하세요 (예: VPN 뒤의 작은 CORS 프록시).', cancel: '취소', save: '저장', saveReload: '저장 후 새로고침', repoLink: 'GitHub', repoAbout: '소스 코드, 업데이트 및 문제 제보', }, pl: { menuSettings: 'Steam Region Bypass — Ustawienia', loading: 'Ładowanie strony sklepu bez cookies konta…', stillBlockedProxy: 'Nadal zablokowane. IP proxy prawdopodobnie jest w ograniczonym regionie — spróbuj innego węzła wyjściowego.', stillBlockedNoProxy: 'Nadal zablokowane przez IP/region. Włącz proxy gateway w ustawieniach i skieruj go na region bez ograniczeń.', ageGate: 'Steam zwrócił weryfikację wieku. Spróbuj ponownie — cookies birthtime są wysyłane automatycznie; jeśli nadal się pojawia, otwórz aplikację raz i potwierdź wiek.', noContent: 'Pobrana strona nie zawierała treści sklepu. Sprawdź ustawienia proxy lub spróbuj ponownie.', failedLoad: 'Nie udało się załadować: {error}', networkError: 'Błąd sieci', requestTimeout: 'Przekroczono limit czasu', retry: 'Ponów', settings: 'Ustawienia', badge: 'Region Bypass', bannerBlockedBadge: 'Region zablokowany', bannerTitle: 'Ta gra jest niedostępna w Twoim regionie', bannerBody: 'Strona sklepu jest wyświetlana przez anonimowe pobranie gościa (bez cookies konta)', viaProxy: 'przez proxy gateway', viaCache: 'z pamięci podręcznej', reload: 'Odśwież', btnTitle: 'Steam Region Bypass', btnText: 'Region Bypass', proxyOn: 'Proxy gateway WŁ', proxyOff: 'Proxy gateway WYŁ', panelTitle: 'Region Bypass', panelSubtitle: 'Anonimowe pobieranie sklepu · opcjonalny proxy gateway', tabGeneral: 'Ogólne', tabSearch: 'Szukaj', tabProxy: 'Proxy', close: 'Zamknij', bypassTrigger: 'Na zablokowanych stronach', bypassTriggerAuto: 'Zastępuj stronę automatycznie', bypassTriggerButton: 'Pokaż przycisk do zastąpienia', bypassTriggerHint: 'Tryb automatyczny od razu zastępuje stronę Oops. Tryb z przyciskiem zostawia stronę błędu i dodaje kontrolkę do uruchomienia obejścia.', bypassOffer: 'Ta strona jest zablokowana w Twoim regionie. Załadować stronę sklepu bez cookies konta?', bypassNow: 'Pokaż stronę sklepu', storeCountry: 'Kraj sklepu (cc)', storeCountryHint: 'Opcjonalne nadpisanie kraju sklepu Steam dla zapytań gościa.', cacheMinutes: 'Czas pamięci podręcznej (minuty)', cacheMinutesHint: 'Jak długo ponownie używać udanej strony gościa przed ponownym pobraniem. 0 wyłącza cache. Odśwież zawsze pobiera świeże dane.', searchUnblocked: 'Podpowiedzi wyszukiwania gościa', searchUnblockedHint: 'Podpowiedzi w nagłówku są pobierane bez cookies konta (ten sam stos gościa co przy omijaniu stron aplikacji). Domyślnie włączone.', searchPageUnblocked: 'Strona /search gościa', searchPageUnblockedHint: 'Zastępuje wyniki Steam /search anonimowym pobraniem gościa. Domyślnie wyłączone — włącz, jeśli potrzebujesz tytułów zablokowanych regionalnie na pełnej stronie wyszukiwania.', suggestGuestNotice: 'Wyszukiwanie gościa Region Bypass jest włączone — wyniki bez cookies konta; mogą być widoczne tytuły zablokowane regionalnie.', suggestGuestSettings: 'Wyłącz w Ustawienia → Wyszukiwanie', suggestEmpty: 'Wpisz, aby przeszukać sklep jako gość (bez cookies konta)', suggestLoading: 'Wyszukiwanie…', suggestFailed: 'Nie udało się załadować podpowiedzi: {error}', suggestNoResults: 'Brak wyników', suggestTypeGame: 'Gra', suggestTypeApp: 'Aplikacja', suggestTypeSoftware: 'Oprogramowanie', suggestTypeDlc: 'DLC', suggestTypeBundle: 'Pakiet', suggestFree: 'Darmowe', suggestControllerFull: 'Pełne wsparcie pada', suggestControllerPartial: 'Częściowe wsparcie pada', suggestAppId: 'ID aplikacji {id}', suggestMetascore: 'Metascore {score}', suggestReviews: '{summary} · {percent}%', suggestRegionBlocked: 'Region zablokowany', suggestComingSoon: 'Wkrótce', suggestStatAvailable: 'Dostępne {count}', suggestStatBlocked: 'Zablokowane {count}', rememberBlockedApps: 'Zapamiętuj zablokowane gry', rememberBlockedAppsHint: 'Gdy Steam pokaże błąd regionu na stronie aplikacji, zapisz ID lokalnie do podświetlenia w wyszukiwaniu gościa.', markBlockedInSearch: 'Podświetlaj w wyszukiwaniu', markBlockedInSearchHint: 'Pokazuj odznakę zablokowanego regionu dla zapisanych gier w wyszukiwaniu gościa.', probeBlockedInSearch: 'Wykrywaj zablokowane w wyszukiwaniu', probeBlockedInSearchHint: 'Po wyrenderowaniu wyszukiwania gościa otwiera strony aplikacji z cookies Steam. Gry z blokadą regionu trafiają na listę. Wymagane logowanie.', probeBlockedScope: 'Sprawdzaj w', probeBlockedScopeBoth: 'Podpowiedzi i strona wyszukiwania', probeBlockedScopeSuggest: 'Tylko podpowiedzi', probeBlockedScopeSearch: 'Tylko strona wyszukiwania', probeBlockedConcurrency: 'Równoległe sprawdzenia', probeBlockedConcurrencyHint: 'Ile stron aplikacji sprawdzać naraz (mniej = łagodniej dla Steam).', probeProgress: 'Sprawdzanie blokad regionu… {done}/{total}', probeFound: 'Dodano zablokowanych gier: {count}', probeNeedLogin: 'Zaloguj się do Steam, aby automatycznie wykrywać zablokowane gry.', probeNeedRemember: 'Włącz «Zapamiętuj zablokowane gry», aby zapisywać wykryte pozycje.', blockedAppsCount: 'Zapisane zablokowane gry: {count}', viewBlockedApps: 'Pokaż listę', hideBlockedApps: 'Ukryj listę', blockedAppsEmpty: 'Brak zapisanych zablokowanych gier.', blockedAppUntitled: 'Aplikacja {id}', clearBlockedApps: 'Wyczyść listę', searchPageLoading: 'Ładowanie wyników wyszukiwania bez cookies konta…', searchPageBanner: 'Wyszukiwanie gościa Region Bypass (anonimowe pobranie)', searchPageBannerSettings: 'Ustawienia', searchPageNoContent: 'Brak wyników w odpowiedzi gościa. Sprawdź proxy lub spróbuj ponownie.', useProxy: 'Użyj proxy gateway', on: 'WŁ', off: 'WYŁ', gatewayMode: 'Tryb gateway', host: 'Host', port: 'Port', username: 'Nazwa użytkownika', password: 'Hasło', optional: 'opcjonalne', proxyHint: 'Userscripts nie mogą ustawić systemowego proxy SOCKS/HTTP. Wskaż te pola na lokalny/zdalny gateway HTTP, który pobiera URL z innego regionu (np. mały proxy CORS za VPN).', cancel: 'Anuluj', save: 'Zapisz', saveReload: 'Zapisz i odśwież stronę', repoLink: 'GitHub', repoAbout: 'Kod źródłowy, aktualizacje i zgłoszenia błędów', }, }; const LOCALE = detectLocale(); /** * @param {string} key * @param {Record} [vars] */ function t(key, vars) { const dict = MESSAGES[LOCALE] || MESSAGES.en; let str = dict[key] ?? MESSAGES.en[key] ?? key; if (vars) { for (const [name, value] of Object.entries(vars)) { str = str.replaceAll('{' + name + '}', String(value)); } } return str; } function detectLocale() { const candidates = [...(navigator.languages || []), navigator.language || 'en'].filter(Boolean); for (const raw of candidates) { const tag = String(raw).replace(/_/g, '-'); const lower = tag.toLowerCase(); const exact = SUPPORTED_LOCALES.find((l) => l.toLowerCase() === lower); if (exact) return exact; if (lower.startsWith('zh')) return 'zh-CN'; if (lower.startsWith('pt')) return 'pt-BR'; const primary = lower.split('-')[0]; const byPrimary = SUPPORTED_LOCALES.find((l) => l.toLowerCase().split('-')[0] === primary); if (byPrimary) return byPrimary; } return 'en'; } function getCookie(name) { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const match = document.cookie.match(new RegExp('(?:^|;\\s*)' + escaped + '=([^;]*)')); return match ? decodeURIComponent(match[1].trim()) : ''; } /** * Store page language for guest fetch: Steam_Language cookie, else UI/browser fallback. * @returns {string} */ function getSteamStoreLanguage() { const fromCookie = getCookie('Steam_Language'); if (fromCookie) return fromCookie; return STEAM_LANG_BY_LOCALE[LOCALE] || 'english'; } /** Settings override, else steamCountry cookie / page hints, else US. */ function getStoreCountryCode() { if (settings.countryCode) { return settings.countryCode.trim().toUpperCase(); } const fromCookie = getCookie('steamCountry'); if (fromCookie) { const cc = decodeURIComponent(fromCookie).split('|')[0]?.trim(); if (/^[A-Za-z]{2}$/.test(cc)) return cc.toUpperCase(); } const ccMatch = document.documentElement.innerHTML.match(/[?&]cc=([A-Za-z]{2})\b/); if (ccMatch) return ccMatch[1].toUpperCase(); return 'US'; } const REGION_PATTERNS = [ /unavailable in your region/i, /not available in your (?:country|region)/i, /недоступн[аоы].*(?:регион|стране|вашем регионе)/i, /в вашем регионе недоступн/i, /este artículo no está disponible en tu región/i, /cet article n'est pas disponible dans votre région/i, ]; /** @type {ReturnType} */ let settings = loadSettings(); let panelOpen = false; /** Guest search state */ let suggestToken = 0; let suggestDebounceTimer = null; let activeSuggestIndex = -1; let searchPageToken = 0; let searchPageDebounceTimer = null; /** href last successfully loaded by guest /search inject (avoids MutationObserver reload loops) */ let searchPageLoadedHref = ''; /** start offset last loaded by guest /search (Steam paginates via start, not only href) */ let searchPageLoadedStart = -1; /** True while loadGuestSearchPage is in flight (skip AJAX-pagination observers) */ let searchPageLoading = false; /** location.href + app ids currently decorated/probed */ let searchResultsFingerprint = ''; let searchResultsRefreshTimer = null; let searchResultsObserverHooked = false; let historyHooked = false; /** @type {Set | null} */ let blockedAppsIndex = null; /** Session cache of probed app IDs that were not region-blocked: id → status */ const probeSessionCache = new Map(); /** @type {Array} */ let lastSuggestItems = []; /** @type {Map} */ const suggestMetaCache = new Map(); init(); function init() { GM_addStyle(getStyles()); try { GM_setValue('srbb_search_term', ''); } catch { /* ignore */ } GM_registerMenuCommand(t('menuSettings'), () => { ensureSettingsButton().then(() => togglePanel(true)); }); ensureSettingsButton(); observeHeader(); initSearchUnblocked(); if (isRegionBlockedPage()) { rememberBlockedAppFromPage(); if (settings.autoBypass) { bypassRegionBlock(); } else { showBypassOffer(); } return; } } function loadSettings() { let raw = GM_getValue(STORAGE_KEY, null); raw = migrateSearchDefaultOn(raw); if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }; const merged = { ...DEFAULT_SETTINGS, ...raw }; delete merged.rememberSearchTerm; merged.cacheMinutes = normalizeCacheMinutes(merged.cacheMinutes); merged.probeBlockedScope = normalizeProbeScope(merged.probeBlockedScope); merged.probeBlockedConcurrency = normalizeProbeConcurrency(merged.probeBlockedConcurrency); return merged; } function migrateSearchDefaultOn(raw) { if (GM_getValue(SEARCH_DEFAULT_ON_FLAG, false)) return raw; GM_setValue(SEARCH_DEFAULT_ON_FLAG, true); if (!raw || typeof raw !== 'object') return raw; if (raw.searchUnblocked === true) return raw; const next = { ...raw, searchUnblocked: true }; GM_setValue(STORAGE_KEY, next); return next; } function saveSettings(next) { const prevSearch = settings.searchUnblocked; const prevSearchPage = settings.searchPageUnblocked; settings = { ...settings, ...next }; settings.cacheMinutes = normalizeCacheMinutes(settings.cacheMinutes); settings.probeBlockedScope = normalizeProbeScope(settings.probeBlockedScope); settings.probeBlockedConcurrency = normalizeProbeConcurrency(settings.probeBlockedConcurrency); GM_setValue(STORAGE_KEY, settings); updateButtonState(); const searchChanged = ('searchUnblocked' in next && prevSearch !== settings.searchUnblocked) || ('searchPageUnblocked' in next && prevSearchPage !== settings.searchPageUnblocked); if (searchChanged) { syncSearchGuestMode(); syncSearchPanelToggle(); if (settings.searchPageUnblocked && isSearchPage()) { searchPageLoadedHref = ''; searchPageLoadedStart = -1; scheduleGuestSearchReload({ immediate: true }); } } } function normalizeCacheMinutes(value) { const n = Math.round(Number(value)); if (!Number.isFinite(n) || n < 0) return DEFAULT_SETTINGS.cacheMinutes; return Math.min(n, CACHE_MINUTES_MAX); } function normalizeProbeScope(value) { return value === 'suggest' || value === 'search' || value === 'both' ? value : DEFAULT_SETTINGS.probeBlockedScope; } function normalizeProbeConcurrency(value) { const n = Math.round(Number(value)); if (!Number.isFinite(n)) return DEFAULT_SETTINGS.probeBlockedConcurrency; return Math.min(PROBE_CONCURRENCY_MAX, Math.max(1, n)); } function getCacheTtlMs() { const minutes = normalizeCacheMinutes(settings.cacheMinutes); return minutes > 0 ? minutes * 60 * 1000 : 0; } function buildCacheKey(targetUrl) { const proxySig = settings.proxyEnabled ? [settings.proxyMode || 'gateway', settings.proxyHost.trim(), String(settings.proxyPort || '').trim()].join('|') : 'direct'; return `${targetUrl}\n${proxySig}`; } function loadCacheStore() { const raw = GM_getValue(CACHE_STORAGE_KEY, null); if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}; return raw; } function pruneCacheStore(store, ttlMs) { const now = Date.now(); const entries = Object.entries(store).filter(([, entry]) => { return ( entry && typeof entry === 'object' && typeof entry.html === 'string' && entry.html.length > 0 && typeof entry.savedAt === 'number' && (ttlMs <= 0 || now - entry.savedAt < ttlMs) ); }); entries.sort((a, b) => b[1].savedAt - a[1].savedAt); const next = {}; for (const [key, entry] of entries.slice(0, CACHE_MAX_ENTRIES)) { next[key] = entry; } return next; } function readPageCache(targetUrl) { const ttlMs = getCacheTtlMs(); if (ttlMs <= 0) return null; const key = buildCacheKey(targetUrl); let store = loadCacheStore(); const entry = store[key]; if (!entry || typeof entry.html !== 'string' || !entry.html) return null; if (Date.now() - entry.savedAt >= ttlMs) { delete store[key]; GM_setValue(CACHE_STORAGE_KEY, pruneCacheStore(store, ttlMs)); return null; } return entry.html; } function writePageCache(targetUrl, html) { const ttlMs = getCacheTtlMs(); if (ttlMs <= 0 || !html) return; const key = buildCacheKey(targetUrl); const store = loadCacheStore(); store[key] = { html, savedAt: Date.now() }; GM_setValue(CACHE_STORAGE_KEY, pruneCacheStore(store, ttlMs)); } function invalidatePageCache(targetUrl) { const key = buildCacheKey(targetUrl); const store = loadCacheStore(); if (!(key in store)) return; delete store[key]; GM_setValue(CACHE_STORAGE_KEY, store); } function loadBlockedAppsStore() { const raw = GM_getValue(BLOCKED_APPS_STORAGE_KEY, null); if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}; return raw; } function pruneBlockedAppsStore(store) { const entries = Object.entries(store).filter(([id, entry]) => { return /^[0-9]+$/.test(id) && entry && typeof entry === 'object' && typeof entry.at === 'number'; }); entries.sort((a, b) => b[1].at - a[1].at); const next = {}; for (const [id, entry] of entries.slice(0, BLOCKED_APPS_MAX_ENTRIES)) { next[id] = entry; } return next; } function saveBlockedAppsStore(store) { GM_setValue(BLOCKED_APPS_STORAGE_KEY, pruneBlockedAppsStore(store)); invalidateBlockedAppsIndex(); } function invalidateBlockedAppsIndex() { blockedAppsIndex = null; } function getBlockedAppIds() { if (!blockedAppsIndex) { blockedAppsIndex = new Set(Object.keys(loadBlockedAppsStore())); } return blockedAppsIndex; } function getBlockedAppsCount() { return getBlockedAppIds().size; } /** @returns {Array<{id:string,name:string,at:number}>} newest first */ function listBlockedAppsEntries() { const store = pruneBlockedAppsStore(loadBlockedAppsStore()); return Object.entries(store) .map(([id, entry]) => ({ id, name: String(entry?.name || '').trim(), at: typeof entry?.at === 'number' ? entry.at : 0, })) .sort((a, b) => b.at - a.at); } function isBlockedApp(appId) { if (!appId) return false; return getBlockedAppIds().has(String(appId)); } function rememberBlockedApp(appId, name = '') { if (!settings.rememberBlockedApps || !appId) return; const id = String(appId); const store = loadBlockedAppsStore(); const prev = store[id] && typeof store[id] === 'object' ? store[id] : {}; const cleanName = String(name || prev.name || '').trim(); store[id] = { name: cleanName, at: Date.now(), }; saveBlockedAppsStore(store); syncBlockedAppsPanel(); } function touchBlockedAppName(appId, name = '') { if (!settings.rememberBlockedApps || !appId) return; const id = String(appId); const cleanName = String(name || '').trim(); if (!cleanName) return; const store = loadBlockedAppsStore(); if (!store[id]) return; if (store[id].name === cleanName) return; store[id] = { ...store[id], name: cleanName, at: Date.now() }; saveBlockedAppsStore(store); } function clearBlockedApps() { GM_setValue(BLOCKED_APPS_STORAGE_KEY, {}); invalidateBlockedAppsIndex(); const listEl = document.getElementById('srbb-blocked-list'); if (listEl) { listEl.hidden = true; listEl.innerHTML = ''; } syncBlockedAppsPanel(); } function getAppNameFromUrl(url = location.href) { const match = String(url).match(/\/app\/\d+\/([^/?#]+)/i); if (!match) return ''; try { return decodeURIComponent(match[1]).replace(/_/g, ' ').trim(); } catch { return match[1].replace(/_/g, ' ').trim(); } } function rememberBlockedAppFromPage() { const appId = getAppIdFromUrl(); if (!appId) return; const name = getAppNameFromUrl() || document.querySelector('.apphub_AppName, #appHubAppName')?.textContent?.trim() || ''; rememberBlockedApp(appId, name); } function isRegionBlockedPage(root = document) { const errorEl = root.querySelector('#error_box .error, #error_box'); if (errorEl) { const text = errorEl.textContent || ''; if (REGION_PATTERNS.some((re) => re.test(text))) return true; } // Error landing without #error_box (localized Oops shell) const oops = root.querySelector('.pageheader'); if (oops && /oops/i.test(oops.textContent || '')) { const bodyText = root.querySelector('.page_header_ctn, #error_box, .page_content')?.textContent || ''; if (REGION_PATTERNS.some((re) => re.test(bodyText))) return true; } return false; } function getAppIdFromUrl(url = location.href) { const match = String(url).match(/\/app\/(\d+)/i); return match ? match[1] : null; } function prepareSuggestItems(items) { if (!settings.markBlockedInSearch) return items; return items.map((item) => ({ ...item, regionBlocked: isBlockedApp(item.id), })); } function buildSuggestBlockedBadgeHtml(item) { if (!item.regionBlocked) return ''; return `${escapeHtml(t('suggestRegionBlocked'))}`; } /** Steam search page size (usually 25). */ function getSearchPageSize(sourceUrl = location.href) { try { const count = parseInt(new URL(sourceUrl, location.origin).searchParams.get('count') || '', 10); if (Number.isFinite(count) && count > 0 && count <= 100) return count; } catch { /* ignore */ } return 25; } /** Current page number from pagination UI (1-based), if present. */ function getActiveSearchPageFromDom() { const pag = document.querySelector('.search_pagination_right'); if (!pag) return null; for (const el of pag.children) { if (el.tagName !== 'SPAN') continue; const n = parseInt(String(el.textContent || '').trim(), 10); if (Number.isFinite(n) && n > 0) return n; } return null; } /** * Steam AJAX search uses `start`/`count`. Browser URL often only has `page=N`, * which the store HTML endpoint ignores — always derive a real offset. */ function getSearchStartOffset(sourceUrl = location.href) { try { const url = new URL(sourceUrl, location.origin); const start = parseInt(url.searchParams.get('start') || '', 10); if (Number.isFinite(start) && start >= 0) return start; const page = parseInt(url.searchParams.get('page') || '', 10); if (Number.isFinite(page) && page >= 1) { return (page - 1) * getSearchPageSize(sourceUrl); } } catch { /* ignore */ } const domPage = getActiveSearchPageFromDom(); if (domPage && domPage >= 1) return (domPage - 1) * getSearchPageSize(sourceUrl); return 0; } function buildTargetUrl(sourceUrl = location.href) { const url = new URL(sourceUrl); url.searchParams.delete('snr'); url.searchParams.set('l', getSteamStoreLanguage()); const cc = getStoreCountryCode(); if (cc) { url.searchParams.set('cc', cc.toLowerCase()); } if (/\/search\/?/i.test(url.pathname)) { url.searchParams.set('ignore_preferences', '1'); const count = getSearchPageSize(url.toString()); const start = getSearchStartOffset(url.toString()); url.searchParams.set('count', String(count)); if (start > 0) { url.searchParams.set('start', String(start)); url.searchParams.set('page', String(Math.floor(start / count) + 1)); } else { url.searchParams.delete('start'); // Keep explicit page=1 out of the request — Steam treats missing as first page if (url.searchParams.get('page') === '1') url.searchParams.delete('page'); } } return url.toString(); } /** * Builds the request URL. When proxy is on, routes through a local/remote HTTP gateway * (Violentmonkey cannot attach a system SOCKS/HTTP proxy to GM_xmlhttpRequest). */ function buildRequestUrl(targetUrl) { if (!settings.proxyEnabled || !settings.proxyHost.trim()) { return targetUrl; } const base = buildProxyBase(); const mode = settings.proxyMode || 'gateway'; if (mode === 'path') { // e.g. http://127.0.0.1:8080/store.steampowered.com/app/412020/ return `${base}/${targetUrl.replace(/^https?:\/\//, '')}`; } if (mode === 'query') { return `${base}/?url=${encodeURIComponent(targetUrl)}`; } // gateway: http://127.0.0.1:8080/https://store.steampowered.com/... return `${base}/${targetUrl}`; } function buildProxyBase() { let host = settings.proxyHost.trim().replace(/\/+$/, ''); const port = String(settings.proxyPort || '').trim(); if (/^https?:\/\//i.test(host)) { if (port && !/:\d+$/.test(host.replace(/^https?:\/\//i, '').split('/')[0])) { return `${host}:${port}`; } return host; } return port ? `http://${host}:${port}` : `http://${host}`; } function buildRequestHeaders() { const steamLang = getSteamStoreLanguage(); const headers = { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': ACCEPT_LANG_BY_STEAM[steamLang] || navigator.language || 'en-US,en;q=0.9', }; // Age gate + store language + optional country without sending login session const cookies = [ 'birthtime=-3338496000', 'mature_content=1', 'wants_mature_content=1', 'lastagecheckage=1-0-1980', `Steam_Language=${steamLang}`, ]; if (settings.countryCode) { const cc = settings.countryCode.trim().toUpperCase(); cookies.push(`steamCountry=${encodeURIComponent(cc + '|0')}`); } else { const cc = getStoreCountryCode(); if (cc) cookies.push(`steamCountry=${encodeURIComponent(cc + '|0')}`); } headers.Cookie = cookies.join('; '); return headers; } function gmRequest(url) { return new Promise((resolve, reject) => { const opts = { method: 'GET', url, anonymous: true, timeout: 45000, headers: buildRequestHeaders(), onload: (res) => resolve(res), onerror: (res) => reject(new Error(res && res.statusText ? res.statusText : t('networkError'))), ontimeout: () => reject(new Error(t('requestTimeout'))), }; if (settings.proxyEnabled && settings.proxyUser) { opts.user = settings.proxyUser; opts.password = settings.proxyPass || ''; } GM_xmlhttpRequest(opts); }); } /** * Account-session request (browser cookies). Never use proxy / guest Cookie override — * we need the logged-in store region view to detect locks. */ function gmSessionRequest(url) { return new Promise((resolve, reject) => { const steamLang = getSteamStoreLanguage(); GM_xmlhttpRequest({ method: 'GET', url, anonymous: false, timeout: 25000, headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': ACCEPT_LANG_BY_STEAM[steamLang] || navigator.language || 'en-US,en;q=0.9', }, onload: (res) => resolve(res), onerror: (res) => reject(new Error(res && res.statusText ? res.statusText : t('networkError'))), ontimeout: () => reject(new Error(t('requestTimeout'))), }); }); } function shouldProbeBlockedScope(scope) { if (!settings.probeBlockedInSearch) return false; if (!settings.rememberBlockedApps) return false; if (!isHostLoggedIn()) return false; const s = normalizeProbeScope(settings.probeBlockedScope); return s === 'both' || s === scope; } async function mapPool(items, limit, fn) { const results = new Array(items.length); let nextIndex = 0; const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length || 1)) }, async () => { while (nextIndex < items.length) { const idx = nextIndex++; results[idx] = await fn(items[idx], idx); } }); await Promise.all(workers); return results; } async function probeAppRegionStatus(appId) { const targetUrl = buildTargetUrl(`https://store.steampowered.com/app/${appId}/`); const response = await gmSessionRequest(targetUrl); if (response.status < 200 || response.status >= 400) return 'unknown'; const doc = new DOMParser().parseFromString(response.responseText || '', 'text/html'); if (isRegionBlockedPage(doc)) return 'blocked'; if (doc.querySelector('#agecheck_form, .agegate_birthday_desc, #app_agegate')) { return 'agegate'; } if (extractGamePageRoot(doc)) return 'ok'; return 'unknown'; } function dedupeProbeApps(apps) { const seen = new Set(); const out = []; for (const app of apps) { const id = app && app.id != null ? String(app.id) : ''; if (!/^\d+$/.test(id) || seen.has(id)) continue; seen.add(id); out.push({ id, name: String(app.name || '').trim(), row: app.row || null }); } return out; } /** * @param {Array<{id:string,name?:string,row?:Element|null}>} apps * @param {{ isCancelled?: () => boolean, onProgress?: Function, onBlocked?: Function, onItemStart?: Function, onItemDone?: Function }} hooks */ async function runBlockedProbe(apps, hooks = {}) { const queue = dedupeProbeApps(apps).filter((app) => { if (isBlockedApp(app.id)) return false; if (probeSessionCache.has(app.id)) return false; return true; }); const total = queue.length; let done = 0; let found = 0; if (!total) { hooks.onProgress?.({ done: 0, total: 0, found: 0 }); return { found: 0, checked: 0 }; } hooks.onProgress?.({ done: 0, total, found: 0 }); await mapPool(queue, normalizeProbeConcurrency(settings.probeBlockedConcurrency), async (app) => { if (hooks.isCancelled?.()) return; hooks.onItemStart?.(app); try { const status = await probeAppRegionStatus(app.id); if (hooks.isCancelled?.()) return; if (status === 'blocked') { rememberBlockedApp(app.id, app.name); found += 1; hooks.onBlocked?.(app); } else { probeSessionCache.set(app.id, status); } } catch { /* leave uncached for a later retry */ } finally { done += 1; hooks.onItemDone?.(app); hooks.onProgress?.({ done, total, found }); } }); return { found, checked: done }; } async function bypassRegionBlock(options = {}) { const forceRefresh = !!options.forceRefresh; const mount = getContentMount(); if (!mount) return; showLoaderOverlay(t('loading')); try { const targetUrl = buildTargetUrl(); let html = null; let fromCache = false; if (!forceRefresh) { html = readPageCache(targetUrl); fromCache = !!html; } else { invalidatePageCache(targetUrl); } if (!html) { const requestUrl = buildRequestUrl(targetUrl); const response = await gmRequest(requestUrl); if (response.status < 200 || response.status >= 400) { throw new Error(`HTTP ${response.status}`); } html = response.responseText || ''; } const doc = new DOMParser().parseFromString(html, 'text/html'); if (isRegionBlockedPage(doc)) { if (fromCache) invalidatePageCache(targetUrl); showStatus( mount, 'error', settings.proxyEnabled ? t('stillBlockedProxy') : t('stillBlockedNoProxy') ); return; } if (doc.querySelector('#agecheck_form, .agegate_birthday_desc, #app_agegate')) { if (fromCache) invalidatePageCache(targetUrl); showStatus(mount, 'error', t('ageGate')); return; } const remoteGame = extractGamePageRoot(doc); if (!remoteGame) { if (fromCache) invalidatePageCache(targetUrl); showStatus(mount, 'error', t('noContent')); return; } if (!fromCache) { writePageCache(targetUrl, html); } await injectDirect(remoteGame, doc, targetUrl, { fromCache }); hideLoaderOverlay(); } catch (err) { showStatus( mount, 'error', t('failedLoad', { error: err && err.message ? err.message : String(err) }) ); } } /** * Game pages use `.game_page_background > #tabletGrid > .page_content_ctn`. * Region-error pages use `.page_header_ctn` (“Oops”) + a bare error box — never nest into that. */ function extractGamePageRoot(doc) { return ( doc.querySelector('.game_page_background') || doc.querySelector('#tabletGrid') || doc.querySelector('.page_content_ctn[itemscope]') || doc.querySelector('#game_highlights')?.closest('.page_content_ctn') || null ); } function getContentMount() { return ( document.querySelector('#responsive_page_template_content') || document.querySelector('.responsive_page_content') || document.querySelector('#error_box')?.closest('.page_content')?.parentElement || document.body ); } function clearErrorPageContent(template) { template .querySelectorAll( [ '.page_header_ctn', '#error_box', '.srbb-shell', '.srbb-status', '.srbb-banner', '.srbb-injected', '.srbb-iframe-wrap', '.game_page_background', '#tabletGrid', '.page_content_ctn', ].join(', ') ) .forEach((el) => el.remove()); // Tag modal lives outside .game_page_background; drop leftovers from a prior inject document.querySelectorAll('#app_tagging_modal').forEach((el) => el.remove()); // Leftover “Oops” blocks that are not inside page_header_ctn template.querySelectorAll('.pageheader').forEach((h2) => { if (/oops/i.test(h2.textContent || '')) { const block = h2.closest('.page_content') || h2.parentElement; block?.remove(); } }); } function applyAppBodyClasses() { const body = document.body; body.classList.remove('redeemwalletcode'); for (const cls of ['app', 'game_bg', 'menu_background_overlap', 'application']) { body.classList.add(cls); } } function createBanner(options = {}) { const banner = document.createElement('div'); banner.className = 'srbb-banner'; const details = [ escapeHtml(t('bannerBody')), settings.proxyEnabled ? escapeHtml(t('viaProxy')) : '', options.fromCache ? escapeHtml(t('viaCache')) : '', settings.countryCode ? `cc=${settings.countryCode.toUpperCase()}` : '', ] .filter(Boolean) .join(' · '); banner.innerHTML = `
${escapeHtml(t('bannerBlockedBadge'))} ${escapeHtml(t('bannerTitle'))}
${details}
`; banner.querySelector('[data-srbb="reload"]')?.addEventListener('click', () => bypassRegionBlock({ forceRefresh: true }) ); return banner; } function absolutizeUrls(root) { root.querySelectorAll('[src], [href], source[srcset]').forEach((el) => { for (const attr of ['src', 'href']) { const val = el.getAttribute(attr); if (!val || val.startsWith('#') || val.startsWith('javascript:') || val.startsWith('data:') || val.startsWith('blob:')) { continue; } try { el.setAttribute(attr, new URL(val, 'https://store.steampowered.com/').href); } catch { /* ignore */ } } const srcset = el.getAttribute('srcset'); if (srcset) { try { el.setAttribute( 'srcset', srcset .split(',') .map((part) => { const bits = part.trim().split(/\s+/); bits[0] = new URL(bits[0], 'https://store.steampowered.com/').href; return bits.join(' '); }) .join(', ') ); } catch { /* ignore */ } } }); root.querySelectorAll('img[data-src]').forEach((img) => { if (!img.getAttribute('src')) { try { img.setAttribute('src', new URL(img.getAttribute('data-src'), 'https://store.steampowered.com/').href); } catch { img.setAttribute('src', img.getAttribute('data-src')); } } }); } /** * Error pages only load store.css + error.css. * App pages also need game.css, store_game_shared.css, apphub.css, etc. * Without those, purchase blocks and columns render as a raw vertical stack. */ function ensureAppPageStylesheets(remoteDoc) { const head = document.head || document.documentElement; const existing = new Set( [...document.querySelectorAll('link[rel="stylesheet"]')].map((link) => stylesheetKey(link.href)) ); const baseFromPage = detectSteamCssBase(); const toAdd = []; remoteDoc.querySelectorAll('link[rel="stylesheet"]').forEach((link) => { const raw = link.getAttribute('href'); if (!raw || /^(chrome-extension|moz-extension|blob):/i.test(raw)) return; if (/error\.css/i.test(raw)) return; let href; try { href = new URL(raw, 'https://store.steampowered.com/').href; } catch { return; } // Saved/local relative names → map onto Steam CDN using store.css location if (!/steamstatic\.com|steampowered\.com/i.test(href) || href.includes('Steam_files')) { const name = raw.split('/').pop().split('?')[0]; if (!name || !/\.css$/i.test(name)) return; href = resolveSteamStylesheet(name, baseFromPage); } const key = stylesheetKey(href); if (!key || existing.has(key)) return; existing.add(key); toAdd.push(href); }); // Guaranteed fallbacks if the guest HTML somehow omits them for (const name of [ 'store_game_shared.css', 'game.css', 'store_background_shared.css', 'apphub.css', 'user_reviews.css', 'recommended.css', 'user_reviews_rewards.css', 'game_mob.css', ]) { const href = resolveSteamStylesheet(name, baseFromPage); const key = stylesheetKey(href); if (existing.has(key)) continue; existing.add(key); toAdd.push(href); } toAdd.forEach((href) => { const el = document.createElement('link'); el.rel = 'stylesheet'; el.type = 'text/css'; el.href = href; el.dataset.srbbStyle = '1'; head.appendChild(el); }); return toAdd.length; } function stylesheetKey(href) { try { const u = new URL(href, location.href); const file = (u.pathname.split('/').pop() || '').toLowerCase(); return file || u.href; } catch { return String(href || '').toLowerCase(); } } function detectSteamCssBase() { const storeLink = [...document.querySelectorAll('link[rel="stylesheet"]')] .map((l) => l.href) .find((h) => /\/css\/v6\/store\.css/i.test(h) || /\/store\.css(\?|$)/i.test(h)); if (storeLink) { return storeLink.replace(/store\.css(\?.*)?$/i, ''); } return 'https://store.fastly.steamstatic.com/public/css/v6/'; } function resolveSteamStylesheet(fileName, cssV6Base) { const shared = [ 'motiva_sans.css', 'shared_global.css', 'buttons.css', 'shared_responsive.css', 'jquery-ui-1.7.2.custom.css', ]; if (shared.includes(fileName)) { return `https://store.fastly.steamstatic.com/public/shared/css/${fileName}`; } if (fileName === 'apphub.css') { return 'https://community.fastly.steamstatic.com/public/css/skin_1/apphub.css'; } return `${cssV6Base}${fileName}`; } function isExecutableScriptTag(script) { const type = (script.getAttribute('type') || 'text/javascript').trim().toLowerCase(); if (!type || type === 'text/javascript' || type === 'application/javascript' || type === 'text/jscript') { return true; } return false; } function isBlockedScriptSrc(src) { return /^(chrome-extension|moz-extension|blob):/i.test(src || '') || /alikeguardian|steamdb\.info\/ext/i.test(src || ''); } function isBlockedScriptCode(code) { return /alikeguardian|ag_changes|chrome-extension:\/\//i.test(code || ''); } function scriptKey(href) { try { const u = new URL(href, location.href); const parts = u.pathname.split('/').filter(Boolean); return (parts.slice(-3).join('/') || u.href).toLowerCase(); } catch { return String(href || '').toLowerCase(); } } function loadExternalScript(href) { return new Promise((resolve) => { const el = document.createElement('script'); el.src = href; el.async = false; el.dataset.srbbScript = '1'; el.onload = () => resolve(); el.onerror = () => { console.warn('[SRBB] failed to load script', href); resolve(); }; (document.head || document.documentElement).appendChild(el); }); } function runInlineScript(code) { const el = document.createElement('script'); el.dataset.srbbScript = '1'; el.textContent = code; (document.body || document.documentElement).appendChild(el); } function waitForPaint() { return new Promise((resolve) => { requestAnimationFrame(() => requestAnimationFrame(resolve)); }); } /** * Newly injected app CSS must be applied before Steam widgets measure layout. * AdjustVisibleAppTags hides every .app_tag when the container width is still 0. */ async function waitForSrbbStylesheets() { const links = [...document.querySelectorAll('link[data-srbb-style="1"]')]; await Promise.all( links.map( (link) => new Promise((resolve) => { if (link.sheet) { resolve(); return; } const done = () => resolve(); link.addEventListener('load', done, { once: true }); link.addEventListener('error', done, { once: true }); // Sheet may appear between the check and the listeners if (link.sheet) done(); }) ) ); await waitForPaint(); } /** * Re-run Steam tag fitting (and a visible fallback) after layout settles. * Tags ship as display:none; InitAppTagModal → AdjustVisibleAppTags reveals them by width. */ function fixupSteamWidgets() { runInlineScript(` (function () { if (typeof $J === 'undefined') return; function fitTags() { if (typeof AdjustVisibleAppTags === 'function') { $J('.glance_tags.popular_tags, .popular_tags[data-appid], .your_tags[data-appid]').each(function () { AdjustVisibleAppTags($J(this)); }); } $J(window).trigger('resize'); $J('.glance_tags.popular_tags').each(function () { var $tags = $J(this).children('.app_tag:not(.add_button)'); if ($tags.length && $tags.filter(':visible').length === 0) { $tags.show(); } }); } fitTags(); setTimeout(fitTags, 100); setTimeout(fitTags, 400); })(); `); } /** Host store session (header), not the anonymous guest HTML we inject. */ function isHostLoggedIn() { if ( document.querySelector( '#account_pulldown, #account_dropdown, #header_notification_area, #global_actions .user_avatar, #global_actions .playerAvatar' ) ) { return true; } if (document.querySelector('#global_actions a[href*="steamcommunity.com/profiles/"], #global_actions a[href*="steamcommunity.com/id/"]')) { return true; } return false; } /** * Guest app HTML assumes an anonymous viewer (“Sign in to add…”, “You're not signed in!”). * Drop that chrome when the host store session is already logged in. */ function stripGuestSignedOutChrome(root) { if (!isHostLoggedIn()) return; const actions = root.querySelector('#queueActionsCtn') || root.querySelector('.queue_actions_ctn'); if (actions) { actions.querySelectorAll(':scope > p').forEach((p) => { if (p.querySelector('a[href*="/login"]')) p.remove(); }); } root.querySelectorAll('.banner_open_in_steam').forEach((el) => el.remove()); } /** * Error / Oops shells omit app libs (game.js, gamehighlightplayer.js, …). * Load any Steam CDN scripts from the guest document that are not already present. * Skips the React store application bundles — those are already booted on the host page. */ async function ensureAppPageScripts(remoteDoc) { const existing = new Set( [...document.querySelectorAll('script[src]')].map((s) => scriptKey(s.src)).filter(Boolean) ); const toLoad = []; remoteDoc.querySelectorAll('script[src]').forEach((script) => { if (!isExecutableScriptTag(script)) return; const raw = script.getAttribute('src'); if (!raw || isBlockedScriptSrc(raw)) return; let href; try { href = new URL(raw, 'https://store.steampowered.com/').href; } catch { return; } if (!/steamstatic\.com|steampowered\.com/i.test(href)) return; if (/\/javascript\/applications\//i.test(href)) return; const key = scriptKey(href); if (!key || existing.has(key)) return; existing.add(key); toLoad.push(href); }); for (const href of toLoad) { await loadExternalScript(href); } return toLoad.length; } /** * #app_tagging_modal + InitAppTagModal sit near the footer, outside * .game_page_background. ShowAppTagModal is only assigned inside InitAppTagModal. * Guest HTML only ships a Sign In panel on the right; restore the tagging form when * the host session is already logged in so InitAppTagModal can wire it up. */ function upgradeGuestAppTagModalForHostSession(modal) { if (!isHostLoggedIn()) return; const right = modal.querySelector('.app_tag_modal_right'); if (!right || right.querySelector('#app_tag_form')) return; if (!right.querySelector('a[href*="/login"]')) return; // Steam requires new tags in English; match the logged-in store markup. right.innerHTML = `

Tags you've applied to this product:(?)

Enter a new tag in English:

Suitable tags should be terms that other users would find useful to browse by.

Apply a tag you've used on other products:

`; } function injectGuestAppTaggingModal(remoteDoc) { document.querySelectorAll('#app_tagging_modal').forEach((el) => el.remove()); const modal = remoteDoc.querySelector('#app_tagging_modal'); if (!modal) return false; const node = document.importNode(modal, true); node.querySelectorAll('script').forEach((el) => el.remove()); absolutizeUrls(node); upgradeGuestAppTagModalForHostSession(node); (document.body || document.documentElement).appendChild(node); return true; } /** * Page-level bootstraps (GStoreItemData, …) sit in /early , outside * .game_page_background. Widget inits live inside the extracted game tree. * importNode copies