const manifest = {"name":"Unifideck"};
const API_VERSION = 2;
const internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;
if (!internalAPIConnection) {
    throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');
}
let api;
try {
    api = internalAPIConnection.connect(API_VERSION, manifest.name);
}
catch {
    api = internalAPIConnection.connect(1, manifest.name);
    console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);
}
if (api._version != API_VERSION) {
    console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);
}
const call = api.call;
const routerHook = api.routerHook;
const toaster = api.toaster;
const openFilePicker = api.openFilePicker;
const definePlugin = (fn) => {
    return (...args) => {
        return fn(...args);
    };
};

var DefaultContext = {
  color: undefined,
  size: undefined,
  className: undefined,
  style: undefined,
  attr: undefined
};
var IconContext = SP_REACT.createContext && /*#__PURE__*/SP_REACT.createContext(DefaultContext);

var _excluded = ["attr", "size", "title"];
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } } return target; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function Tree2Element(tree) {
  return tree && tree.map((node, i) => /*#__PURE__*/SP_REACT.createElement(node.tag, _objectSpread({
    key: i
  }, node.attr), Tree2Element(node.child)));
}
function GenIcon(data) {
  return props => /*#__PURE__*/SP_REACT.createElement(IconBase, _extends({
    attr: _objectSpread({}, data.attr)
  }, props), Tree2Element(data.child));
}
function IconBase(props) {
  var elem = conf => {
    var {
        attr,
        size,
        title
      } = props,
      svgProps = _objectWithoutProperties(props, _excluded);
    var computedSize = size || conf.size || "1em";
    var className;
    if (conf.className) className = conf.className;
    if (props.className) className = (className ? className + " " : "") + props.className;
    return /*#__PURE__*/SP_REACT.createElement("svg", _extends({
      stroke: "currentColor",
      fill: "currentColor",
      strokeWidth: "0"
    }, conf.attr, attr, svgProps, {
      className: className,
      style: _objectSpread(_objectSpread({
        color: props.color || conf.color
      }, conf.style), props.style),
      height: computedSize,
      width: computedSize,
      xmlns: "http://www.w3.org/2000/svg"
    }), title && /*#__PURE__*/SP_REACT.createElement("title", null, title), props.children);
  };
  return IconContext !== undefined ? /*#__PURE__*/SP_REACT.createElement(IconContext.Consumer, null, conf => elem(conf)) : elem(DefaultContext);
}

// THIS FILE IS AUTO GENERATED
function FaSteam (props) {
  return GenIcon({"attr":{"viewBox":"0 0 496 512"},"child":[{"tag":"path","attr":{"d":"M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z"},"child":[]}]})(props);
}function FaXbox (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M369.9 318.2c44.3 54.3 64.7 98.8 54.4 118.7-7.9 15.1-56.7 44.6-92.6 55.9-29.6 9.3-68.4 13.3-100.4 10.2-38.2-3.7-76.9-17.4-110.1-39C93.3 445.8 87 438.3 87 423.4c0-29.9 32.9-82.3 89.2-142.1 32-33.9 76.5-73.7 81.4-72.6 9.4 2.1 84.3 75.1 112.3 109.5zM188.6 143.8c-29.7-26.9-58.1-53.9-86.4-63.4-15.2-5.1-16.3-4.8-28.7 8.1-29.2 30.4-53.5 79.7-60.3 122.4-5.4 34.2-6.1 43.8-4.2 60.5 5.6 50.5 17.3 85.4 40.5 120.9 9.5 14.6 12.1 17.3 9.3 9.9-4.2-11-.3-37.5 9.5-64 14.3-39 53.9-112.9 120.3-194.4zm311.6 63.5C483.3 127.3 432.7 77 425.6 77c-7.3 0-24.2 6.5-36 13.9-23.3 14.5-41 31.4-64.3 52.8C367.7 197 427.5 283.1 448.2 346c6.8 20.7 9.7 41.1 7.4 52.3-1.7 8.5-1.7 8.5 1.4 4.6 6.1-7.7 19.9-31.3 25.4-43.5 7.4-16.2 15-40.2 18.6-58.7 4.3-22.5 3.9-70.8-.8-93.4zM141.3 43C189 40.5 251 77.5 255.6 78.4c.7.1 10.4-4.2 21.6-9.7 63.9-31.1 94-25.8 107.4-25.2-63.9-39.3-152.7-50-233.9-11.7-23.4 11.1-24 11.9-9.4 11.2z"},"child":[]}]})(props);
}function FaCheckCircle (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"},"child":[]}]})(props);
}function FaCheck (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"},"child":[]}]})(props);
}function FaCloudDownloadAlt (props) {
  return GenIcon({"attr":{"viewBox":"0 0 640 512"},"child":[{"tag":"path","attr":{"d":"M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zm-132.9 88.7L299.3 420.7c-6.2 6.2-16.4 6.2-22.6 0L171.3 315.3c-10.1-10.1-2.9-27.3 11.3-27.3H248V176c0-8.8 7.2-16 16-16h48c8.8 0 16 7.2 16 16v112h65.4c14.2 0 21.4 17.2 11.3 27.3z"},"child":[]}]})(props);
}function FaCloudUploadAlt (props) {
  return GenIcon({"attr":{"viewBox":"0 0 640 512"},"child":[{"tag":"path","attr":{"d":"M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zM393.4 288H328v112c0 8.8-7.2 16-16 16h-48c-8.8 0-16-7.2-16-16V288h-65.4c-14.3 0-21.4-17.2-11.3-27.3l105.4-105.4c6.2-6.2 16.4-6.2 22.6 0l105.4 105.4c10.1 10.1 2.9 27.3-11.3 27.3z"},"child":[]}]})(props);
}function FaCloud (props) {
  return GenIcon({"attr":{"viewBox":"0 0 640 512"},"child":[{"tag":"path","attr":{"d":"M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4z"},"child":[]}]})(props);
}function FaCog (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"},"child":[]}]})(props);
}function FaDesktop (props) {
  return GenIcon({"attr":{"viewBox":"0 0 576 512"},"child":[{"tag":"path","attr":{"d":"M528 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h192l-16 48h-72c-13.3 0-24 10.7-24 24s10.7 24 24 24h272c13.3 0 24-10.7 24-24s-10.7-24-24-24h-72l-16-48h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h448v288z"},"child":[]}]})(props);
}function FaDownload (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z"},"child":[]}]})(props);
}function FaExchangeAlt (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z"},"child":[]}]})(props);
}function FaExclamationTriangle (props) {
  return GenIcon({"attr":{"viewBox":"0 0 576 512"},"child":[{"tag":"path","attr":{"d":"M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"},"child":[]}]})(props);
}function FaExternalLinkAlt (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M432,320H400a16,16,0,0,0-16,16V448H64V128H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V336A16,16,0,0,0,432,320ZM488,0h-128c-21.37,0-32.05,25.91-17,41l35.73,35.73L135,320.37a24,24,0,0,0,0,34L157.67,377a24,24,0,0,0,34,0L435.28,133.32,471,169c15,15,41,4.5,41-17V24A24,24,0,0,0,488,0Z"},"child":[]}]})(props);
}function FaFolderOpen (props) {
  return GenIcon({"attr":{"viewBox":"0 0 576 512"},"child":[{"tag":"path","attr":{"d":"M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"},"child":[]}]})(props);
}function FaForward (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"},"child":[]}]})(props);
}function FaGamepad (props) {
  return GenIcon({"attr":{"viewBox":"0 0 640 512"},"child":[{"tag":"path","attr":{"d":"M480.07 96H160a160 160 0 1 0 114.24 272h91.52A160 160 0 1 0 480.07 96zM248 268a12 12 0 0 1-12 12h-52v52a12 12 0 0 1-12 12h-24a12 12 0 0 1-12-12v-52H84a12 12 0 0 1-12-12v-24a12 12 0 0 1 12-12h52v-52a12 12 0 0 1 12-12h24a12 12 0 0 1 12 12v52h52a12 12 0 0 1 12 12zm216 76a40 40 0 1 1 40-40 40 40 0 0 1-40 40zm64-96a40 40 0 1 1 40-40 40 40 0 0 1-40 40z"},"child":[]}]})(props);
}function FaImage (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"},"child":[]}]})(props);
}function FaLock (props) {
  return GenIcon({"attr":{"viewBox":"0 0 448 512"},"child":[{"tag":"path","attr":{"d":"M400 224h-24v-72C376 68.2 307.8 0 224 0S72 68.2 72 152v72H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48zm-104 0H152v-72c0-39.7 32.3-72 72-72s72 32.3 72 72v72z"},"child":[]}]})(props);
}function FaPlay (props) {
  return GenIcon({"attr":{"viewBox":"0 0 448 512"},"child":[{"tag":"path","attr":{"d":"M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"},"child":[]}]})(props);
}function FaSyncAlt (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M370.72 133.28C339.458 104.008 298.888 87.962 255.848 88c-77.458.068-144.328 53.178-162.791 126.85-1.344 5.363-6.122 9.15-11.651 9.15H24.103c-7.498 0-13.194-6.807-11.807-14.176C33.933 94.924 134.813 8 256 8c66.448 0 126.791 26.136 171.315 68.685L463.03 40.97C478.149 25.851 504 36.559 504 57.941V192c0 13.255-10.745 24-24 24H345.941c-21.382 0-32.09-25.851-16.971-40.971l41.75-41.749zM32 296h134.059c21.382 0 32.09 25.851 16.971 40.971l-41.75 41.75c31.262 29.273 71.835 45.319 114.876 45.28 77.418-.07 144.315-53.144 162.787-126.849 1.344-5.363 6.122-9.15 11.651-9.15h57.304c7.498 0 13.194 6.807 11.807 14.176C478.067 417.076 377.187 504 256 504c-66.448 0-126.791-26.136-171.315-68.685L48.97 471.03C33.851 486.149 8 475.441 8 454.059V320c0-13.255 10.745-24 24-24z"},"child":[]}]})(props);
}function FaSync (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M440.65 12.57l4 82.77A247.16 247.16 0 0 0 255.83 8C134.73 8 33.91 94.92 12.29 209.82A12 12 0 0 0 24.09 224h49.05a12 12 0 0 0 11.67-9.26 175.91 175.91 0 0 1 317-56.94l-101.46-4.86a12 12 0 0 0-12.57 12v47.41a12 12 0 0 0 12 12H500a12 12 0 0 0 12-12V12a12 12 0 0 0-12-12h-47.37a12 12 0 0 0-11.98 12.57zM255.83 432a175.61 175.61 0 0 1-146-77.8l101.8 4.87a12 12 0 0 0 12.57-12v-47.4a12 12 0 0 0-12-12H12a12 12 0 0 0-12 12V500a12 12 0 0 0 12 12h47.35a12 12 0 0 0 12-12.6l-4.15-82.57A247.17 247.17 0 0 0 255.83 504c121.11 0 221.93-86.92 243.55-201.82a12 12 0 0 0-11.8-14.18h-49.05a12 12 0 0 0-11.67 9.26A175.86 175.86 0 0 1 255.83 432z"},"child":[]}]})(props);
}function FaTimes (props) {
  return GenIcon({"attr":{"viewBox":"0 0 352 512"},"child":[{"tag":"path","attr":{"d":"M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"},"child":[]}]})(props);
}function FaTrashAlt (props) {
  return GenIcon({"attr":{"viewBox":"0 0 448 512"},"child":[{"tag":"path","attr":{"d":"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"},"child":[]}]})(props);
}function FaTrash (props) {
  return GenIcon({"attr":{"viewBox":"0 0 448 512"},"child":[{"tag":"path","attr":{"d":"M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z"},"child":[]}]})(props);
}function FaTrophy (props) {
  return GenIcon({"attr":{"viewBox":"0 0 576 512"},"child":[{"tag":"path","attr":{"d":"M552 64H448V24c0-13.3-10.7-24-24-24H152c-13.3 0-24 10.7-24 24v40H24C10.7 64 0 74.7 0 88v56c0 35.7 22.5 72.4 61.9 100.7 31.5 22.7 69.8 37.1 110 41.7C203.3 338.5 240 360 240 360v72h-48c-35.3 0-64 20.7-64 56v12c0 6.6 5.4 12 12 12h296c6.6 0 12-5.4 12-12v-12c0-35.3-28.7-56-64-56h-48v-72s36.7-21.5 68.1-73.6c40.3-4.6 78.6-19 110-41.7 39.3-28.3 61.9-65 61.9-100.7V88c0-13.3-10.7-24-24-24zM99.3 192.8C74.9 175.2 64 155.6 64 144v-16h64.2c1 32.6 5.8 61.2 12.8 86.2-15.1-5.2-29.2-12.4-41.7-21.4zM512 144c0 16.1-17.7 36.1-35.3 48.8-12.5 9-26.7 16.2-41.8 21.4 7-25 11.8-53.6 12.8-86.2H512v16z"},"child":[]}]})(props);
}function FaUndo (props) {
  return GenIcon({"attr":{"viewBox":"0 0 512 512"},"child":[{"tag":"path","attr":{"d":"M212.333 224.333H12c-6.627 0-12-5.373-12-12V12C0 5.373 5.373 0 12 0h48c6.627 0 12 5.373 12 12v78.112C117.773 39.279 184.26 7.47 258.175 8.007c136.906.994 246.448 111.623 246.157 248.532C504.041 393.258 393.12 504 256.333 504c-64.089 0-122.496-24.313-166.51-64.215-5.099-4.622-5.334-12.554-.467-17.42l33.967-33.967c4.474-4.474 11.662-4.717 16.401-.525C170.76 415.336 211.58 432 256.333 432c97.268 0 176-78.716 176-176 0-97.267-78.716-176-176-176-58.496 0-110.28 28.476-142.274 72.333h98.274c6.627 0 12 5.373 12 12v48c0 6.627-5.373 12-12 12z"},"child":[]}]})(props);
}

const isString$1 = obj => typeof obj === 'string';
const defer = () => {
  let res;
  let rej;
  const promise = new Promise((resolve, reject) => {
    res = resolve;
    rej = reject;
  });
  promise.resolve = res;
  promise.reject = rej;
  return promise;
};
const makeString = object => {
  if (object == null) return '';
  return '' + object;
};
const copy = (a, s, t) => {
  a.forEach(m => {
    if (s[m]) t[m] = s[m];
  });
};
const lastOfPathSeparatorRegExp = /###/g;
const cleanKey = key => key && key.indexOf('###') > -1 ? key.replace(lastOfPathSeparatorRegExp, '.') : key;
const canNotTraverseDeeper = object => !object || isString$1(object);
const getLastOfPath = (object, path, Empty) => {
  const stack = !isString$1(path) ? path : path.split('.');
  let stackIndex = 0;
  while (stackIndex < stack.length - 1) {
    if (canNotTraverseDeeper(object)) return {};
    const key = cleanKey(stack[stackIndex]);
    if (!object[key] && Empty) object[key] = new Empty();
    if (Object.prototype.hasOwnProperty.call(object, key)) {
      object = object[key];
    } else {
      object = {};
    }
    ++stackIndex;
  }
  if (canNotTraverseDeeper(object)) return {};
  return {
    obj: object,
    k: cleanKey(stack[stackIndex])
  };
};
const setPath = (object, path, newValue) => {
  const {
    obj,
    k
  } = getLastOfPath(object, path, Object);
  if (obj !== undefined || path.length === 1) {
    obj[k] = newValue;
    return;
  }
  let e = path[path.length - 1];
  let p = path.slice(0, path.length - 1);
  let last = getLastOfPath(object, p, Object);
  while (last.obj === undefined && p.length) {
    e = `${p[p.length - 1]}.${e}`;
    p = p.slice(0, p.length - 1);
    last = getLastOfPath(object, p, Object);
    if (last?.obj && typeof last.obj[`${last.k}.${e}`] !== 'undefined') {
      last.obj = undefined;
    }
  }
  last.obj[`${last.k}.${e}`] = newValue;
};
const pushPath = (object, path, newValue, concat) => {
  const {
    obj,
    k
  } = getLastOfPath(object, path, Object);
  obj[k] = obj[k] || [];
  obj[k].push(newValue);
};
const getPath = (object, path) => {
  const {
    obj,
    k
  } = getLastOfPath(object, path);
  if (!obj) return undefined;
  if (!Object.prototype.hasOwnProperty.call(obj, k)) return undefined;
  return obj[k];
};
const getPathWithDefaults = (data, defaultData, key) => {
  const value = getPath(data, key);
  if (value !== undefined) {
    return value;
  }
  return getPath(defaultData, key);
};
const deepExtend = (target, source, overwrite) => {
  for (const prop in source) {
    if (prop !== '__proto__' && prop !== 'constructor') {
      if (prop in target) {
        if (isString$1(target[prop]) || target[prop] instanceof String || isString$1(source[prop]) || source[prop] instanceof String) {
          if (overwrite) target[prop] = source[prop];
        } else {
          deepExtend(target[prop], source[prop], overwrite);
        }
      } else {
        target[prop] = source[prop];
      }
    }
  }
  return target;
};
const regexEscape = str => str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
var _entityMap = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#39;',
  '/': '&#x2F;'
};
const escape = data => {
  if (isString$1(data)) {
    return data.replace(/[&<>"'\/]/g, s => _entityMap[s]);
  }
  return data;
};
class RegExpCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.regExpMap = new Map();
    this.regExpQueue = [];
  }
  getRegExp(pattern) {
    const regExpFromCache = this.regExpMap.get(pattern);
    if (regExpFromCache !== undefined) {
      return regExpFromCache;
    }
    const regExpNew = new RegExp(pattern);
    if (this.regExpQueue.length === this.capacity) {
      this.regExpMap.delete(this.regExpQueue.shift());
    }
    this.regExpMap.set(pattern, regExpNew);
    this.regExpQueue.push(pattern);
    return regExpNew;
  }
}
const chars = [' ', ',', '?', '!', ';'];
const looksLikeObjectPathRegExpCache = new RegExpCache(20);
const looksLikeObjectPath = (key, nsSeparator, keySeparator) => {
  nsSeparator = nsSeparator || '';
  keySeparator = keySeparator || '';
  const possibleChars = chars.filter(c => nsSeparator.indexOf(c) < 0 && keySeparator.indexOf(c) < 0);
  if (possibleChars.length === 0) return true;
  const r = looksLikeObjectPathRegExpCache.getRegExp(`(${possibleChars.map(c => c === '?' ? '\\?' : c).join('|')})`);
  let matched = !r.test(key);
  if (!matched) {
    const ki = key.indexOf(keySeparator);
    if (ki > 0 && !r.test(key.substring(0, ki))) {
      matched = true;
    }
  }
  return matched;
};
const deepFind = (obj, path, keySeparator = '.') => {
  if (!obj) return undefined;
  if (obj[path]) {
    if (!Object.prototype.hasOwnProperty.call(obj, path)) return undefined;
    return obj[path];
  }
  const tokens = path.split(keySeparator);
  let current = obj;
  for (let i = 0; i < tokens.length;) {
    if (!current || typeof current !== 'object') {
      return undefined;
    }
    let next;
    let nextPath = '';
    for (let j = i; j < tokens.length; ++j) {
      if (j !== i) {
        nextPath += keySeparator;
      }
      nextPath += tokens[j];
      next = current[nextPath];
      if (next !== undefined) {
        if (['string', 'number', 'boolean'].indexOf(typeof next) > -1 && j < tokens.length - 1) {
          continue;
        }
        i += j - i + 1;
        break;
      }
    }
    current = next;
  }
  return current;
};
const getCleanedCode = code => code?.replace('_', '-');

const consoleLogger = {
  type: 'logger',
  log(args) {
    this.output('log', args);
  },
  warn(args) {
    this.output('warn', args);
  },
  error(args) {
    this.output('error', args);
  },
  output(type, args) {
    console?.[type]?.apply?.(console, args);
  }
};
class Logger {
  constructor(concreteLogger, options = {}) {
    this.init(concreteLogger, options);
  }
  init(concreteLogger, options = {}) {
    this.prefix = options.prefix || 'i18next:';
    this.logger = concreteLogger || consoleLogger;
    this.options = options;
    this.debug = options.debug;
  }
  log(...args) {
    return this.forward(args, 'log', '', true);
  }
  warn(...args) {
    return this.forward(args, 'warn', '', true);
  }
  error(...args) {
    return this.forward(args, 'error', '');
  }
  deprecate(...args) {
    return this.forward(args, 'warn', 'WARNING DEPRECATED: ', true);
  }
  forward(args, lvl, prefix, debugOnly) {
    if (debugOnly && !this.debug) return null;
    if (isString$1(args[0])) args[0] = `${prefix}${this.prefix} ${args[0]}`;
    return this.logger[lvl](args);
  }
  create(moduleName) {
    return new Logger(this.logger, {
      ...{
        prefix: `${this.prefix}:${moduleName}:`
      },
      ...this.options
    });
  }
  clone(options) {
    options = options || this.options;
    options.prefix = options.prefix || this.prefix;
    return new Logger(this.logger, options);
  }
}
var baseLogger = new Logger();

class EventEmitter {
  constructor() {
    this.observers = {};
  }
  on(events, listener) {
    events.split(' ').forEach(event => {
      if (!this.observers[event]) this.observers[event] = new Map();
      const numListeners = this.observers[event].get(listener) || 0;
      this.observers[event].set(listener, numListeners + 1);
    });
    return this;
  }
  off(event, listener) {
    if (!this.observers[event]) return;
    if (!listener) {
      delete this.observers[event];
      return;
    }
    this.observers[event].delete(listener);
  }
  emit(event, ...args) {
    if (this.observers[event]) {
      const cloned = Array.from(this.observers[event].entries());
      cloned.forEach(([observer, numTimesAdded]) => {
        for (let i = 0; i < numTimesAdded; i++) {
          observer(...args);
        }
      });
    }
    if (this.observers['*']) {
      const cloned = Array.from(this.observers['*'].entries());
      cloned.forEach(([observer, numTimesAdded]) => {
        for (let i = 0; i < numTimesAdded; i++) {
          observer.apply(observer, [event, ...args]);
        }
      });
    }
  }
}

class ResourceStore extends EventEmitter {
  constructor(data, options = {
    ns: ['translation'],
    defaultNS: 'translation'
  }) {
    super();
    this.data = data || {};
    this.options = options;
    if (this.options.keySeparator === undefined) {
      this.options.keySeparator = '.';
    }
    if (this.options.ignoreJSONStructure === undefined) {
      this.options.ignoreJSONStructure = true;
    }
  }
  addNamespaces(ns) {
    if (this.options.ns.indexOf(ns) < 0) {
      this.options.ns.push(ns);
    }
  }
  removeNamespaces(ns) {
    const index = this.options.ns.indexOf(ns);
    if (index > -1) {
      this.options.ns.splice(index, 1);
    }
  }
  getResource(lng, ns, key, options = {}) {
    const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
    const ignoreJSONStructure = options.ignoreJSONStructure !== undefined ? options.ignoreJSONStructure : this.options.ignoreJSONStructure;
    let path;
    if (lng.indexOf('.') > -1) {
      path = lng.split('.');
    } else {
      path = [lng, ns];
      if (key) {
        if (Array.isArray(key)) {
          path.push(...key);
        } else if (isString$1(key) && keySeparator) {
          path.push(...key.split(keySeparator));
        } else {
          path.push(key);
        }
      }
    }
    const result = getPath(this.data, path);
    if (!result && !ns && !key && lng.indexOf('.') > -1) {
      lng = path[0];
      ns = path[1];
      key = path.slice(2).join('.');
    }
    if (result || !ignoreJSONStructure || !isString$1(key)) return result;
    return deepFind(this.data?.[lng]?.[ns], key, keySeparator);
  }
  addResource(lng, ns, key, value, options = {
    silent: false
  }) {
    const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
    let path = [lng, ns];
    if (key) path = path.concat(keySeparator ? key.split(keySeparator) : key);
    if (lng.indexOf('.') > -1) {
      path = lng.split('.');
      value = ns;
      ns = path[1];
    }
    this.addNamespaces(ns);
    setPath(this.data, path, value);
    if (!options.silent) this.emit('added', lng, ns, key, value);
  }
  addResources(lng, ns, resources, options = {
    silent: false
  }) {
    for (const m in resources) {
      if (isString$1(resources[m]) || Array.isArray(resources[m])) this.addResource(lng, ns, m, resources[m], {
        silent: true
      });
    }
    if (!options.silent) this.emit('added', lng, ns, resources);
  }
  addResourceBundle(lng, ns, resources, deep, overwrite, options = {
    silent: false,
    skipCopy: false
  }) {
    let path = [lng, ns];
    if (lng.indexOf('.') > -1) {
      path = lng.split('.');
      deep = resources;
      resources = ns;
      ns = path[1];
    }
    this.addNamespaces(ns);
    let pack = getPath(this.data, path) || {};
    if (!options.skipCopy) resources = JSON.parse(JSON.stringify(resources));
    if (deep) {
      deepExtend(pack, resources, overwrite);
    } else {
      pack = {
        ...pack,
        ...resources
      };
    }
    setPath(this.data, path, pack);
    if (!options.silent) this.emit('added', lng, ns, resources);
  }
  removeResourceBundle(lng, ns) {
    if (this.hasResourceBundle(lng, ns)) {
      delete this.data[lng][ns];
    }
    this.removeNamespaces(ns);
    this.emit('removed', lng, ns);
  }
  hasResourceBundle(lng, ns) {
    return this.getResource(lng, ns) !== undefined;
  }
  getResourceBundle(lng, ns) {
    if (!ns) ns = this.options.defaultNS;
    return this.getResource(lng, ns);
  }
  getDataByLanguage(lng) {
    return this.data[lng];
  }
  hasLanguageSomeTranslations(lng) {
    const data = this.getDataByLanguage(lng);
    const n = data && Object.keys(data) || [];
    return !!n.find(v => data[v] && Object.keys(data[v]).length > 0);
  }
  toJSON() {
    return this.data;
  }
}

var postProcessor = {
  processors: {},
  addPostProcessor(module) {
    this.processors[module.name] = module;
  },
  handle(processors, value, key, options, translator) {
    processors.forEach(processor => {
      value = this.processors[processor]?.process(value, key, options, translator) ?? value;
    });
    return value;
  }
};

const PATH_KEY = Symbol('i18next/PATH_KEY');
function createProxy() {
  const state = [];
  const handler = Object.create(null);
  let proxy;
  handler.get = (target, key) => {
    proxy?.revoke?.();
    if (key === PATH_KEY) return state;
    state.push(key);
    proxy = Proxy.revocable(target, handler);
    return proxy.proxy;
  };
  return Proxy.revocable(Object.create(null), handler).proxy;
}
function keysFromSelector(selector, opts) {
  const {
    [PATH_KEY]: path
  } = selector(createProxy());
  return path.join(opts?.keySeparator ?? '.');
}

const checkedLoadedFor = {};
const shouldHandleAsObject = res => !isString$1(res) && typeof res !== 'boolean' && typeof res !== 'number';
class Translator extends EventEmitter {
  constructor(services, options = {}) {
    super();
    copy(['resourceStore', 'languageUtils', 'pluralResolver', 'interpolator', 'backendConnector', 'i18nFormat', 'utils'], services, this);
    this.options = options;
    if (this.options.keySeparator === undefined) {
      this.options.keySeparator = '.';
    }
    this.logger = baseLogger.create('translator');
  }
  changeLanguage(lng) {
    if (lng) this.language = lng;
  }
  exists(key, o = {
    interpolation: {}
  }) {
    const opt = {
      ...o
    };
    if (key == null) return false;
    const resolved = this.resolve(key, opt);
    if (resolved?.res === undefined) return false;
    const isObject = shouldHandleAsObject(resolved.res);
    if (opt.returnObjects === false && isObject) {
      return false;
    }
    return true;
  }
  extractFromKey(key, opt) {
    let nsSeparator = opt.nsSeparator !== undefined ? opt.nsSeparator : this.options.nsSeparator;
    if (nsSeparator === undefined) nsSeparator = ':';
    const keySeparator = opt.keySeparator !== undefined ? opt.keySeparator : this.options.keySeparator;
    let namespaces = opt.ns || this.options.defaultNS || [];
    const wouldCheckForNsInKey = nsSeparator && key.indexOf(nsSeparator) > -1;
    const seemsNaturalLanguage = !this.options.userDefinedKeySeparator && !opt.keySeparator && !this.options.userDefinedNsSeparator && !opt.nsSeparator && !looksLikeObjectPath(key, nsSeparator, keySeparator);
    if (wouldCheckForNsInKey && !seemsNaturalLanguage) {
      const m = key.match(this.interpolator.nestingRegexp);
      if (m && m.length > 0) {
        return {
          key,
          namespaces: isString$1(namespaces) ? [namespaces] : namespaces
        };
      }
      const parts = key.split(nsSeparator);
      if (nsSeparator !== keySeparator || nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1) namespaces = parts.shift();
      key = parts.join(keySeparator);
    }
    return {
      key,
      namespaces: isString$1(namespaces) ? [namespaces] : namespaces
    };
  }
  translate(keys, o, lastKey) {
    let opt = typeof o === 'object' ? {
      ...o
    } : o;
    if (typeof opt !== 'object' && this.options.overloadTranslationOptionHandler) {
      opt = this.options.overloadTranslationOptionHandler(arguments);
    }
    if (typeof opt === 'object') opt = {
      ...opt
    };
    if (!opt) opt = {};
    if (keys == null) return '';
    if (typeof keys === 'function') keys = keysFromSelector(keys, {
      ...this.options,
      ...opt
    });
    if (!Array.isArray(keys)) keys = [String(keys)];
    const returnDetails = opt.returnDetails !== undefined ? opt.returnDetails : this.options.returnDetails;
    const keySeparator = opt.keySeparator !== undefined ? opt.keySeparator : this.options.keySeparator;
    const {
      key,
      namespaces
    } = this.extractFromKey(keys[keys.length - 1], opt);
    const namespace = namespaces[namespaces.length - 1];
    let nsSeparator = opt.nsSeparator !== undefined ? opt.nsSeparator : this.options.nsSeparator;
    if (nsSeparator === undefined) nsSeparator = ':';
    const lng = opt.lng || this.language;
    const appendNamespaceToCIMode = opt.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode;
    if (lng?.toLowerCase() === 'cimode') {
      if (appendNamespaceToCIMode) {
        if (returnDetails) {
          return {
            res: `${namespace}${nsSeparator}${key}`,
            usedKey: key,
            exactUsedKey: key,
            usedLng: lng,
            usedNS: namespace,
            usedParams: this.getUsedParamsDetails(opt)
          };
        }
        return `${namespace}${nsSeparator}${key}`;
      }
      if (returnDetails) {
        return {
          res: key,
          usedKey: key,
          exactUsedKey: key,
          usedLng: lng,
          usedNS: namespace,
          usedParams: this.getUsedParamsDetails(opt)
        };
      }
      return key;
    }
    const resolved = this.resolve(keys, opt);
    let res = resolved?.res;
    const resUsedKey = resolved?.usedKey || key;
    const resExactUsedKey = resolved?.exactUsedKey || key;
    const noObject = ['[object Number]', '[object Function]', '[object RegExp]'];
    const joinArrays = opt.joinArrays !== undefined ? opt.joinArrays : this.options.joinArrays;
    const handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject;
    const needsPluralHandling = opt.count !== undefined && !isString$1(opt.count);
    const hasDefaultValue = Translator.hasDefaultValue(opt);
    const defaultValueSuffix = needsPluralHandling ? this.pluralResolver.getSuffix(lng, opt.count, opt) : '';
    const defaultValueSuffixOrdinalFallback = opt.ordinal && needsPluralHandling ? this.pluralResolver.getSuffix(lng, opt.count, {
      ordinal: false
    }) : '';
    const needsZeroSuffixLookup = needsPluralHandling && !opt.ordinal && opt.count === 0;
    const defaultValue = needsZeroSuffixLookup && opt[`defaultValue${this.options.pluralSeparator}zero`] || opt[`defaultValue${defaultValueSuffix}`] || opt[`defaultValue${defaultValueSuffixOrdinalFallback}`] || opt.defaultValue;
    let resForObjHndl = res;
    if (handleAsObjectInI18nFormat && !res && hasDefaultValue) {
      resForObjHndl = defaultValue;
    }
    const handleAsObject = shouldHandleAsObject(resForObjHndl);
    const resType = Object.prototype.toString.apply(resForObjHndl);
    if (handleAsObjectInI18nFormat && resForObjHndl && handleAsObject && noObject.indexOf(resType) < 0 && !(isString$1(joinArrays) && Array.isArray(resForObjHndl))) {
      if (!opt.returnObjects && !this.options.returnObjects) {
        if (!this.options.returnedObjectHandler) {
          this.logger.warn('accessing an object - but returnObjects options is not enabled!');
        }
        const r = this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, resForObjHndl, {
          ...opt,
          ns: namespaces
        }) : `key '${key} (${this.language})' returned an object instead of string.`;
        if (returnDetails) {
          resolved.res = r;
          resolved.usedParams = this.getUsedParamsDetails(opt);
          return resolved;
        }
        return r;
      }
      if (keySeparator) {
        const resTypeIsArray = Array.isArray(resForObjHndl);
        const copy = resTypeIsArray ? [] : {};
        const newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey;
        for (const m in resForObjHndl) {
          if (Object.prototype.hasOwnProperty.call(resForObjHndl, m)) {
            const deepKey = `${newKeyToUse}${keySeparator}${m}`;
            if (hasDefaultValue && !res) {
              copy[m] = this.translate(deepKey, {
                ...opt,
                defaultValue: shouldHandleAsObject(defaultValue) ? defaultValue[m] : undefined,
                ...{
                  joinArrays: false,
                  ns: namespaces
                }
              });
            } else {
              copy[m] = this.translate(deepKey, {
                ...opt,
                ...{
                  joinArrays: false,
                  ns: namespaces
                }
              });
            }
            if (copy[m] === deepKey) copy[m] = resForObjHndl[m];
          }
        }
        res = copy;
      }
    } else if (handleAsObjectInI18nFormat && isString$1(joinArrays) && Array.isArray(res)) {
      res = res.join(joinArrays);
      if (res) res = this.extendTranslation(res, keys, opt, lastKey);
    } else {
      let usedDefault = false;
      let usedKey = false;
      if (!this.isValidLookup(res) && hasDefaultValue) {
        usedDefault = true;
        res = defaultValue;
      }
      if (!this.isValidLookup(res)) {
        usedKey = true;
        res = key;
      }
      const missingKeyNoValueFallbackToKey = opt.missingKeyNoValueFallbackToKey || this.options.missingKeyNoValueFallbackToKey;
      const resForMissing = missingKeyNoValueFallbackToKey && usedKey ? undefined : res;
      const updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing;
      if (usedKey || usedDefault || updateMissing) {
        this.logger.log(updateMissing ? 'updateKey' : 'missingKey', lng, namespace, key, updateMissing ? defaultValue : res);
        if (keySeparator) {
          const fk = this.resolve(key, {
            ...opt,
            keySeparator: false
          });
          if (fk && fk.res) this.logger.warn('Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.');
        }
        let lngs = [];
        const fallbackLngs = this.languageUtils.getFallbackCodes(this.options.fallbackLng, opt.lng || this.language);
        if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) {
          for (let i = 0; i < fallbackLngs.length; i++) {
            lngs.push(fallbackLngs[i]);
          }
        } else if (this.options.saveMissingTo === 'all') {
          lngs = this.languageUtils.toResolveHierarchy(opt.lng || this.language);
        } else {
          lngs.push(opt.lng || this.language);
        }
        const send = (l, k, specificDefaultValue) => {
          const defaultForMissing = hasDefaultValue && specificDefaultValue !== res ? specificDefaultValue : resForMissing;
          if (this.options.missingKeyHandler) {
            this.options.missingKeyHandler(l, namespace, k, defaultForMissing, updateMissing, opt);
          } else if (this.backendConnector?.saveMissing) {
            this.backendConnector.saveMissing(l, namespace, k, defaultForMissing, updateMissing, opt);
          }
          this.emit('missingKey', l, namespace, k, res);
        };
        if (this.options.saveMissing) {
          if (this.options.saveMissingPlurals && needsPluralHandling) {
            lngs.forEach(language => {
              const suffixes = this.pluralResolver.getSuffixes(language, opt);
              if (needsZeroSuffixLookup && opt[`defaultValue${this.options.pluralSeparator}zero`] && suffixes.indexOf(`${this.options.pluralSeparator}zero`) < 0) {
                suffixes.push(`${this.options.pluralSeparator}zero`);
              }
              suffixes.forEach(suffix => {
                send([language], key + suffix, opt[`defaultValue${suffix}`] || defaultValue);
              });
            });
          } else {
            send(lngs, key, defaultValue);
          }
        }
      }
      res = this.extendTranslation(res, keys, opt, resolved, lastKey);
      if (usedKey && res === key && this.options.appendNamespaceToMissingKey) {
        res = `${namespace}${nsSeparator}${key}`;
      }
      if ((usedKey || usedDefault) && this.options.parseMissingKeyHandler) {
        res = this.options.parseMissingKeyHandler(this.options.appendNamespaceToMissingKey ? `${namespace}${nsSeparator}${key}` : key, usedDefault ? res : undefined, opt);
      }
    }
    if (returnDetails) {
      resolved.res = res;
      resolved.usedParams = this.getUsedParamsDetails(opt);
      return resolved;
    }
    return res;
  }
  extendTranslation(res, key, opt, resolved, lastKey) {
    if (this.i18nFormat?.parse) {
      res = this.i18nFormat.parse(res, {
        ...this.options.interpolation.defaultVariables,
        ...opt
      }, opt.lng || this.language || resolved.usedLng, resolved.usedNS, resolved.usedKey, {
        resolved
      });
    } else if (!opt.skipInterpolation) {
      if (opt.interpolation) this.interpolator.init({
        ...opt,
        ...{
          interpolation: {
            ...this.options.interpolation,
            ...opt.interpolation
          }
        }
      });
      const skipOnVariables = isString$1(res) && (opt?.interpolation?.skipOnVariables !== undefined ? opt.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables);
      let nestBef;
      if (skipOnVariables) {
        const nb = res.match(this.interpolator.nestingRegexp);
        nestBef = nb && nb.length;
      }
      let data = opt.replace && !isString$1(opt.replace) ? opt.replace : opt;
      if (this.options.interpolation.defaultVariables) data = {
        ...this.options.interpolation.defaultVariables,
        ...data
      };
      res = this.interpolator.interpolate(res, data, opt.lng || this.language || resolved.usedLng, opt);
      if (skipOnVariables) {
        const na = res.match(this.interpolator.nestingRegexp);
        const nestAft = na && na.length;
        if (nestBef < nestAft) opt.nest = false;
      }
      if (!opt.lng && resolved && resolved.res) opt.lng = this.language || resolved.usedLng;
      if (opt.nest !== false) res = this.interpolator.nest(res, (...args) => {
        if (lastKey?.[0] === args[0] && !opt.context) {
          this.logger.warn(`It seems you are nesting recursively key: ${args[0]} in key: ${key[0]}`);
          return null;
        }
        return this.translate(...args, key);
      }, opt);
      if (opt.interpolation) this.interpolator.reset();
    }
    const postProcess = opt.postProcess || this.options.postProcess;
    const postProcessorNames = isString$1(postProcess) ? [postProcess] : postProcess;
    if (res != null && postProcessorNames?.length && opt.applyPostProcessor !== false) {
      res = postProcessor.handle(postProcessorNames, res, key, this.options && this.options.postProcessPassResolved ? {
        i18nResolved: {
          ...resolved,
          usedParams: this.getUsedParamsDetails(opt)
        },
        ...opt
      } : opt, this);
    }
    return res;
  }
  resolve(keys, opt = {}) {
    let found;
    let usedKey;
    let exactUsedKey;
    let usedLng;
    let usedNS;
    if (isString$1(keys)) keys = [keys];
    keys.forEach(k => {
      if (this.isValidLookup(found)) return;
      const extracted = this.extractFromKey(k, opt);
      const key = extracted.key;
      usedKey = key;
      let namespaces = extracted.namespaces;
      if (this.options.fallbackNS) namespaces = namespaces.concat(this.options.fallbackNS);
      const needsPluralHandling = opt.count !== undefined && !isString$1(opt.count);
      const needsZeroSuffixLookup = needsPluralHandling && !opt.ordinal && opt.count === 0;
      const needsContextHandling = opt.context !== undefined && (isString$1(opt.context) || typeof opt.context === 'number') && opt.context !== '';
      const codes = opt.lngs ? opt.lngs : this.languageUtils.toResolveHierarchy(opt.lng || this.language, opt.fallbackLng);
      namespaces.forEach(ns => {
        if (this.isValidLookup(found)) return;
        usedNS = ns;
        if (!checkedLoadedFor[`${codes[0]}-${ns}`] && this.utils?.hasLoadedNamespace && !this.utils?.hasLoadedNamespace(usedNS)) {
          checkedLoadedFor[`${codes[0]}-${ns}`] = true;
          this.logger.warn(`key "${usedKey}" for languages "${codes.join(', ')}" won't get resolved as namespace "${usedNS}" was not yet loaded`, 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!');
        }
        codes.forEach(code => {
          if (this.isValidLookup(found)) return;
          usedLng = code;
          const finalKeys = [key];
          if (this.i18nFormat?.addLookupKeys) {
            this.i18nFormat.addLookupKeys(finalKeys, key, code, ns, opt);
          } else {
            let pluralSuffix;
            if (needsPluralHandling) pluralSuffix = this.pluralResolver.getSuffix(code, opt.count, opt);
            const zeroSuffix = `${this.options.pluralSeparator}zero`;
            const ordinalPrefix = `${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;
            if (needsPluralHandling) {
              if (opt.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) {
                finalKeys.push(key + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator));
              }
              finalKeys.push(key + pluralSuffix);
              if (needsZeroSuffixLookup) {
                finalKeys.push(key + zeroSuffix);
              }
            }
            if (needsContextHandling) {
              const contextKey = `${key}${this.options.contextSeparator || '_'}${opt.context}`;
              finalKeys.push(contextKey);
              if (needsPluralHandling) {
                if (opt.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) {
                  finalKeys.push(contextKey + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator));
                }
                finalKeys.push(contextKey + pluralSuffix);
                if (needsZeroSuffixLookup) {
                  finalKeys.push(contextKey + zeroSuffix);
                }
              }
            }
          }
          let possibleKey;
          while (possibleKey = finalKeys.pop()) {
            if (!this.isValidLookup(found)) {
              exactUsedKey = possibleKey;
              found = this.getResource(code, ns, possibleKey, opt);
            }
          }
        });
      });
    });
    return {
      res: found,
      usedKey,
      exactUsedKey,
      usedLng,
      usedNS
    };
  }
  isValidLookup(res) {
    return res !== undefined && !(!this.options.returnNull && res === null) && !(!this.options.returnEmptyString && res === '');
  }
  getResource(code, ns, key, options = {}) {
    if (this.i18nFormat?.getResource) return this.i18nFormat.getResource(code, ns, key, options);
    return this.resourceStore.getResource(code, ns, key, options);
  }
  getUsedParamsDetails(options = {}) {
    const optionsKeys = ['defaultValue', 'ordinal', 'context', 'replace', 'lng', 'lngs', 'fallbackLng', 'ns', 'keySeparator', 'nsSeparator', 'returnObjects', 'returnDetails', 'joinArrays', 'postProcess', 'interpolation'];
    const useOptionsReplaceForData = options.replace && !isString$1(options.replace);
    let data = useOptionsReplaceForData ? options.replace : options;
    if (useOptionsReplaceForData && typeof options.count !== 'undefined') {
      data.count = options.count;
    }
    if (this.options.interpolation.defaultVariables) {
      data = {
        ...this.options.interpolation.defaultVariables,
        ...data
      };
    }
    if (!useOptionsReplaceForData) {
      data = {
        ...data
      };
      for (const key of optionsKeys) {
        delete data[key];
      }
    }
    return data;
  }
  static hasDefaultValue(options) {
    const prefix = 'defaultValue';
    for (const option in options) {
      if (Object.prototype.hasOwnProperty.call(options, option) && prefix === option.substring(0, prefix.length) && undefined !== options[option]) {
        return true;
      }
    }
    return false;
  }
}

class LanguageUtil {
  constructor(options) {
    this.options = options;
    this.supportedLngs = this.options.supportedLngs || false;
    this.logger = baseLogger.create('languageUtils');
  }
  getScriptPartFromCode(code) {
    code = getCleanedCode(code);
    if (!code || code.indexOf('-') < 0) return null;
    const p = code.split('-');
    if (p.length === 2) return null;
    p.pop();
    if (p[p.length - 1].toLowerCase() === 'x') return null;
    return this.formatLanguageCode(p.join('-'));
  }
  getLanguagePartFromCode(code) {
    code = getCleanedCode(code);
    if (!code || code.indexOf('-') < 0) return code;
    const p = code.split('-');
    return this.formatLanguageCode(p[0]);
  }
  formatLanguageCode(code) {
    if (isString$1(code) && code.indexOf('-') > -1) {
      let formattedCode;
      try {
        formattedCode = Intl.getCanonicalLocales(code)[0];
      } catch (e) {}
      if (formattedCode && this.options.lowerCaseLng) {
        formattedCode = formattedCode.toLowerCase();
      }
      if (formattedCode) return formattedCode;
      if (this.options.lowerCaseLng) {
        return code.toLowerCase();
      }
      return code;
    }
    return this.options.cleanCode || this.options.lowerCaseLng ? code.toLowerCase() : code;
  }
  isSupportedCode(code) {
    if (this.options.load === 'languageOnly' || this.options.nonExplicitSupportedLngs) {
      code = this.getLanguagePartFromCode(code);
    }
    return !this.supportedLngs || !this.supportedLngs.length || this.supportedLngs.indexOf(code) > -1;
  }
  getBestMatchFromCodes(codes) {
    if (!codes) return null;
    let found;
    codes.forEach(code => {
      if (found) return;
      const cleanedLng = this.formatLanguageCode(code);
      if (!this.options.supportedLngs || this.isSupportedCode(cleanedLng)) found = cleanedLng;
    });
    if (!found && this.options.supportedLngs) {
      codes.forEach(code => {
        if (found) return;
        const lngScOnly = this.getScriptPartFromCode(code);
        if (this.isSupportedCode(lngScOnly)) return found = lngScOnly;
        const lngOnly = this.getLanguagePartFromCode(code);
        if (this.isSupportedCode(lngOnly)) return found = lngOnly;
        found = this.options.supportedLngs.find(supportedLng => {
          if (supportedLng === lngOnly) return supportedLng;
          if (supportedLng.indexOf('-') < 0 && lngOnly.indexOf('-') < 0) return;
          if (supportedLng.indexOf('-') > 0 && lngOnly.indexOf('-') < 0 && supportedLng.substring(0, supportedLng.indexOf('-')) === lngOnly) return supportedLng;
          if (supportedLng.indexOf(lngOnly) === 0 && lngOnly.length > 1) return supportedLng;
        });
      });
    }
    if (!found) found = this.getFallbackCodes(this.options.fallbackLng)[0];
    return found;
  }
  getFallbackCodes(fallbacks, code) {
    if (!fallbacks) return [];
    if (typeof fallbacks === 'function') fallbacks = fallbacks(code);
    if (isString$1(fallbacks)) fallbacks = [fallbacks];
    if (Array.isArray(fallbacks)) return fallbacks;
    if (!code) return fallbacks.default || [];
    let found = fallbacks[code];
    if (!found) found = fallbacks[this.getScriptPartFromCode(code)];
    if (!found) found = fallbacks[this.formatLanguageCode(code)];
    if (!found) found = fallbacks[this.getLanguagePartFromCode(code)];
    if (!found) found = fallbacks.default;
    return found || [];
  }
  toResolveHierarchy(code, fallbackCode) {
    const fallbackCodes = this.getFallbackCodes((fallbackCode === false ? [] : fallbackCode) || this.options.fallbackLng || [], code);
    const codes = [];
    const addCode = c => {
      if (!c) return;
      if (this.isSupportedCode(c)) {
        codes.push(c);
      } else {
        this.logger.warn(`rejecting language code not found in supportedLngs: ${c}`);
      }
    };
    if (isString$1(code) && (code.indexOf('-') > -1 || code.indexOf('_') > -1)) {
      if (this.options.load !== 'languageOnly') addCode(this.formatLanguageCode(code));
      if (this.options.load !== 'languageOnly' && this.options.load !== 'currentOnly') addCode(this.getScriptPartFromCode(code));
      if (this.options.load !== 'currentOnly') addCode(this.getLanguagePartFromCode(code));
    } else if (isString$1(code)) {
      addCode(this.formatLanguageCode(code));
    }
    fallbackCodes.forEach(fc => {
      if (codes.indexOf(fc) < 0) addCode(this.formatLanguageCode(fc));
    });
    return codes;
  }
}

const suffixesOrder = {
  zero: 0,
  one: 1,
  two: 2,
  few: 3,
  many: 4,
  other: 5
};
const dummyRule = {
  select: count => count === 1 ? 'one' : 'other',
  resolvedOptions: () => ({
    pluralCategories: ['one', 'other']
  })
};
class PluralResolver {
  constructor(languageUtils, options = {}) {
    this.languageUtils = languageUtils;
    this.options = options;
    this.logger = baseLogger.create('pluralResolver');
    this.pluralRulesCache = {};
  }
  clearCache() {
    this.pluralRulesCache = {};
  }
  getRule(code, options = {}) {
    const cleanedCode = getCleanedCode(code === 'dev' ? 'en' : code);
    const type = options.ordinal ? 'ordinal' : 'cardinal';
    const cacheKey = JSON.stringify({
      cleanedCode,
      type
    });
    if (cacheKey in this.pluralRulesCache) {
      return this.pluralRulesCache[cacheKey];
    }
    let rule;
    try {
      rule = new Intl.PluralRules(cleanedCode, {
        type
      });
    } catch (err) {
      if (!Intl) {
        this.logger.error('No Intl support, please use an Intl polyfill!');
        return dummyRule;
      }
      if (!code.match(/-|_/)) return dummyRule;
      const lngPart = this.languageUtils.getLanguagePartFromCode(code);
      rule = this.getRule(lngPart, options);
    }
    this.pluralRulesCache[cacheKey] = rule;
    return rule;
  }
  needsPlural(code, options = {}) {
    let rule = this.getRule(code, options);
    if (!rule) rule = this.getRule('dev', options);
    return rule?.resolvedOptions().pluralCategories.length > 1;
  }
  getPluralFormsOfKey(code, key, options = {}) {
    return this.getSuffixes(code, options).map(suffix => `${key}${suffix}`);
  }
  getSuffixes(code, options = {}) {
    let rule = this.getRule(code, options);
    if (!rule) rule = this.getRule('dev', options);
    if (!rule) return [];
    return rule.resolvedOptions().pluralCategories.sort((pluralCategory1, pluralCategory2) => suffixesOrder[pluralCategory1] - suffixesOrder[pluralCategory2]).map(pluralCategory => `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${pluralCategory}`);
  }
  getSuffix(code, count, options = {}) {
    const rule = this.getRule(code, options);
    if (rule) {
      return `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${rule.select(count)}`;
    }
    this.logger.warn(`no plural rule found for: ${code}`);
    return this.getSuffix('dev', count, options);
  }
}

const deepFindWithDefaults = (data, defaultData, key, keySeparator = '.', ignoreJSONStructure = true) => {
  let path = getPathWithDefaults(data, defaultData, key);
  if (!path && ignoreJSONStructure && isString$1(key)) {
    path = deepFind(data, key, keySeparator);
    if (path === undefined) path = deepFind(defaultData, key, keySeparator);
  }
  return path;
};
const regexSafe = val => val.replace(/\$/g, '$$$$');
class Interpolator {
  constructor(options = {}) {
    this.logger = baseLogger.create('interpolator');
    this.options = options;
    this.format = options?.interpolation?.format || (value => value);
    this.init(options);
  }
  init(options = {}) {
    if (!options.interpolation) options.interpolation = {
      escapeValue: true
    };
    const {
      escape: escape$1,
      escapeValue,
      useRawValueToEscape,
      prefix,
      prefixEscaped,
      suffix,
      suffixEscaped,
      formatSeparator,
      unescapeSuffix,
      unescapePrefix,
      nestingPrefix,
      nestingPrefixEscaped,
      nestingSuffix,
      nestingSuffixEscaped,
      nestingOptionsSeparator,
      maxReplaces,
      alwaysFormat
    } = options.interpolation;
    this.escape = escape$1 !== undefined ? escape$1 : escape;
    this.escapeValue = escapeValue !== undefined ? escapeValue : true;
    this.useRawValueToEscape = useRawValueToEscape !== undefined ? useRawValueToEscape : false;
    this.prefix = prefix ? regexEscape(prefix) : prefixEscaped || '{{';
    this.suffix = suffix ? regexEscape(suffix) : suffixEscaped || '}}';
    this.formatSeparator = formatSeparator || ',';
    this.unescapePrefix = unescapeSuffix ? '' : unescapePrefix || '-';
    this.unescapeSuffix = this.unescapePrefix ? '' : unescapeSuffix || '';
    this.nestingPrefix = nestingPrefix ? regexEscape(nestingPrefix) : nestingPrefixEscaped || regexEscape('$t(');
    this.nestingSuffix = nestingSuffix ? regexEscape(nestingSuffix) : nestingSuffixEscaped || regexEscape(')');
    this.nestingOptionsSeparator = nestingOptionsSeparator || ',';
    this.maxReplaces = maxReplaces || 1000;
    this.alwaysFormat = alwaysFormat !== undefined ? alwaysFormat : false;
    this.resetRegExp();
  }
  reset() {
    if (this.options) this.init(this.options);
  }
  resetRegExp() {
    const getOrResetRegExp = (existingRegExp, pattern) => {
      if (existingRegExp?.source === pattern) {
        existingRegExp.lastIndex = 0;
        return existingRegExp;
      }
      return new RegExp(pattern, 'g');
    };
    this.regexp = getOrResetRegExp(this.regexp, `${this.prefix}(.+?)${this.suffix}`);
    this.regexpUnescape = getOrResetRegExp(this.regexpUnescape, `${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`);
    this.nestingRegexp = getOrResetRegExp(this.nestingRegexp, `${this.nestingPrefix}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${this.nestingSuffix}`);
  }
  interpolate(str, data, lng, options) {
    let match;
    let value;
    let replaces;
    const defaultData = this.options && this.options.interpolation && this.options.interpolation.defaultVariables || {};
    const handleFormat = key => {
      if (key.indexOf(this.formatSeparator) < 0) {
        const path = deepFindWithDefaults(data, defaultData, key, this.options.keySeparator, this.options.ignoreJSONStructure);
        return this.alwaysFormat ? this.format(path, undefined, lng, {
          ...options,
          ...data,
          interpolationkey: key
        }) : path;
      }
      const p = key.split(this.formatSeparator);
      const k = p.shift().trim();
      const f = p.join(this.formatSeparator).trim();
      return this.format(deepFindWithDefaults(data, defaultData, k, this.options.keySeparator, this.options.ignoreJSONStructure), f, lng, {
        ...options,
        ...data,
        interpolationkey: k
      });
    };
    this.resetRegExp();
    const missingInterpolationHandler = options?.missingInterpolationHandler || this.options.missingInterpolationHandler;
    const skipOnVariables = options?.interpolation?.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables;
    const todos = [{
      regex: this.regexpUnescape,
      safeValue: val => regexSafe(val)
    }, {
      regex: this.regexp,
      safeValue: val => this.escapeValue ? regexSafe(this.escape(val)) : regexSafe(val)
    }];
    todos.forEach(todo => {
      replaces = 0;
      while (match = todo.regex.exec(str)) {
        const matchedVar = match[1].trim();
        value = handleFormat(matchedVar);
        if (value === undefined) {
          if (typeof missingInterpolationHandler === 'function') {
            const temp = missingInterpolationHandler(str, match, options);
            value = isString$1(temp) ? temp : '';
          } else if (options && Object.prototype.hasOwnProperty.call(options, matchedVar)) {
            value = '';
          } else if (skipOnVariables) {
            value = match[0];
            continue;
          } else {
            this.logger.warn(`missed to pass in variable ${matchedVar} for interpolating ${str}`);
            value = '';
          }
        } else if (!isString$1(value) && !this.useRawValueToEscape) {
          value = makeString(value);
        }
        const safeValue = todo.safeValue(value);
        str = str.replace(match[0], safeValue);
        if (skipOnVariables) {
          todo.regex.lastIndex += value.length;
          todo.regex.lastIndex -= match[0].length;
        } else {
          todo.regex.lastIndex = 0;
        }
        replaces++;
        if (replaces >= this.maxReplaces) {
          break;
        }
      }
    });
    return str;
  }
  nest(str, fc, options = {}) {
    let match;
    let value;
    let clonedOptions;
    const handleHasOptions = (key, inheritedOptions) => {
      const sep = this.nestingOptionsSeparator;
      if (key.indexOf(sep) < 0) return key;
      const c = key.split(new RegExp(`${sep}[ ]*{`));
      let optionsString = `{${c[1]}`;
      key = c[0];
      optionsString = this.interpolate(optionsString, clonedOptions);
      const matchedSingleQuotes = optionsString.match(/'/g);
      const matchedDoubleQuotes = optionsString.match(/"/g);
      if ((matchedSingleQuotes?.length ?? 0) % 2 === 0 && !matchedDoubleQuotes || matchedDoubleQuotes.length % 2 !== 0) {
        optionsString = optionsString.replace(/'/g, '"');
      }
      try {
        clonedOptions = JSON.parse(optionsString);
        if (inheritedOptions) clonedOptions = {
          ...inheritedOptions,
          ...clonedOptions
        };
      } catch (e) {
        this.logger.warn(`failed parsing options string in nesting for key ${key}`, e);
        return `${key}${sep}${optionsString}`;
      }
      if (clonedOptions.defaultValue && clonedOptions.defaultValue.indexOf(this.prefix) > -1) delete clonedOptions.defaultValue;
      return key;
    };
    while (match = this.nestingRegexp.exec(str)) {
      let formatters = [];
      clonedOptions = {
        ...options
      };
      clonedOptions = clonedOptions.replace && !isString$1(clonedOptions.replace) ? clonedOptions.replace : clonedOptions;
      clonedOptions.applyPostProcessor = false;
      delete clonedOptions.defaultValue;
      const keyEndIndex = /{.*}/.test(match[1]) ? match[1].lastIndexOf('}') + 1 : match[1].indexOf(this.formatSeparator);
      if (keyEndIndex !== -1) {
        formatters = match[1].slice(keyEndIndex).split(this.formatSeparator).map(elem => elem.trim()).filter(Boolean);
        match[1] = match[1].slice(0, keyEndIndex);
      }
      value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions);
      if (value && match[0] === str && !isString$1(value)) return value;
      if (!isString$1(value)) value = makeString(value);
      if (!value) {
        this.logger.warn(`missed to resolve ${match[1]} for nesting ${str}`);
        value = '';
      }
      if (formatters.length) {
        value = formatters.reduce((v, f) => this.format(v, f, options.lng, {
          ...options,
          interpolationkey: match[1].trim()
        }), value.trim());
      }
      str = str.replace(match[0], value);
      this.regexp.lastIndex = 0;
    }
    return str;
  }
}

const parseFormatStr = formatStr => {
  let formatName = formatStr.toLowerCase().trim();
  const formatOptions = {};
  if (formatStr.indexOf('(') > -1) {
    const p = formatStr.split('(');
    formatName = p[0].toLowerCase().trim();
    const optStr = p[1].substring(0, p[1].length - 1);
    if (formatName === 'currency' && optStr.indexOf(':') < 0) {
      if (!formatOptions.currency) formatOptions.currency = optStr.trim();
    } else if (formatName === 'relativetime' && optStr.indexOf(':') < 0) {
      if (!formatOptions.range) formatOptions.range = optStr.trim();
    } else {
      const opts = optStr.split(';');
      opts.forEach(opt => {
        if (opt) {
          const [key, ...rest] = opt.split(':');
          const val = rest.join(':').trim().replace(/^'+|'+$/g, '');
          const trimmedKey = key.trim();
          if (!formatOptions[trimmedKey]) formatOptions[trimmedKey] = val;
          if (val === 'false') formatOptions[trimmedKey] = false;
          if (val === 'true') formatOptions[trimmedKey] = true;
          if (!isNaN(val)) formatOptions[trimmedKey] = parseInt(val, 10);
        }
      });
    }
  }
  return {
    formatName,
    formatOptions
  };
};
const createCachedFormatter = fn => {
  const cache = {};
  return (v, l, o) => {
    let optForCache = o;
    if (o && o.interpolationkey && o.formatParams && o.formatParams[o.interpolationkey] && o[o.interpolationkey]) {
      optForCache = {
        ...optForCache,
        [o.interpolationkey]: undefined
      };
    }
    const key = l + JSON.stringify(optForCache);
    let frm = cache[key];
    if (!frm) {
      frm = fn(getCleanedCode(l), o);
      cache[key] = frm;
    }
    return frm(v);
  };
};
const createNonCachedFormatter = fn => (v, l, o) => fn(getCleanedCode(l), o)(v);
class Formatter {
  constructor(options = {}) {
    this.logger = baseLogger.create('formatter');
    this.options = options;
    this.init(options);
  }
  init(services, options = {
    interpolation: {}
  }) {
    this.formatSeparator = options.interpolation.formatSeparator || ',';
    const cf = options.cacheInBuiltFormats ? createCachedFormatter : createNonCachedFormatter;
    this.formats = {
      number: cf((lng, opt) => {
        const formatter = new Intl.NumberFormat(lng, {
          ...opt
        });
        return val => formatter.format(val);
      }),
      currency: cf((lng, opt) => {
        const formatter = new Intl.NumberFormat(lng, {
          ...opt,
          style: 'currency'
        });
        return val => formatter.format(val);
      }),
      datetime: cf((lng, opt) => {
        const formatter = new Intl.DateTimeFormat(lng, {
          ...opt
        });
        return val => formatter.format(val);
      }),
      relativetime: cf((lng, opt) => {
        const formatter = new Intl.RelativeTimeFormat(lng, {
          ...opt
        });
        return val => formatter.format(val, opt.range || 'day');
      }),
      list: cf((lng, opt) => {
        const formatter = new Intl.ListFormat(lng, {
          ...opt
        });
        return val => formatter.format(val);
      })
    };
  }
  add(name, fc) {
    this.formats[name.toLowerCase().trim()] = fc;
  }
  addCached(name, fc) {
    this.formats[name.toLowerCase().trim()] = createCachedFormatter(fc);
  }
  format(value, format, lng, options = {}) {
    const formats = format.split(this.formatSeparator);
    if (formats.length > 1 && formats[0].indexOf('(') > 1 && formats[0].indexOf(')') < 0 && formats.find(f => f.indexOf(')') > -1)) {
      const lastIndex = formats.findIndex(f => f.indexOf(')') > -1);
      formats[0] = [formats[0], ...formats.splice(1, lastIndex)].join(this.formatSeparator);
    }
    const result = formats.reduce((mem, f) => {
      const {
        formatName,
        formatOptions
      } = parseFormatStr(f);
      if (this.formats[formatName]) {
        let formatted = mem;
        try {
          const valOptions = options?.formatParams?.[options.interpolationkey] || {};
          const l = valOptions.locale || valOptions.lng || options.locale || options.lng || lng;
          formatted = this.formats[formatName](mem, l, {
            ...formatOptions,
            ...options,
            ...valOptions
          });
        } catch (error) {
          this.logger.warn(error);
        }
        return formatted;
      } else {
        this.logger.warn(`there was no format function for ${formatName}`);
      }
      return mem;
    }, value);
    return result;
  }
}

const removePending = (q, name) => {
  if (q.pending[name] !== undefined) {
    delete q.pending[name];
    q.pendingCount--;
  }
};
class Connector extends EventEmitter {
  constructor(backend, store, services, options = {}) {
    super();
    this.backend = backend;
    this.store = store;
    this.services = services;
    this.languageUtils = services.languageUtils;
    this.options = options;
    this.logger = baseLogger.create('backendConnector');
    this.waitingReads = [];
    this.maxParallelReads = options.maxParallelReads || 10;
    this.readingCalls = 0;
    this.maxRetries = options.maxRetries >= 0 ? options.maxRetries : 5;
    this.retryTimeout = options.retryTimeout >= 1 ? options.retryTimeout : 350;
    this.state = {};
    this.queue = [];
    this.backend?.init?.(services, options.backend, options);
  }
  queueLoad(languages, namespaces, options, callback) {
    const toLoad = {};
    const pending = {};
    const toLoadLanguages = {};
    const toLoadNamespaces = {};
    languages.forEach(lng => {
      let hasAllNamespaces = true;
      namespaces.forEach(ns => {
        const name = `${lng}|${ns}`;
        if (!options.reload && this.store.hasResourceBundle(lng, ns)) {
          this.state[name] = 2;
        } else if (this.state[name] < 0) ; else if (this.state[name] === 1) {
          if (pending[name] === undefined) pending[name] = true;
        } else {
          this.state[name] = 1;
          hasAllNamespaces = false;
          if (pending[name] === undefined) pending[name] = true;
          if (toLoad[name] === undefined) toLoad[name] = true;
          if (toLoadNamespaces[ns] === undefined) toLoadNamespaces[ns] = true;
        }
      });
      if (!hasAllNamespaces) toLoadLanguages[lng] = true;
    });
    if (Object.keys(toLoad).length || Object.keys(pending).length) {
      this.queue.push({
        pending,
        pendingCount: Object.keys(pending).length,
        loaded: {},
        errors: [],
        callback
      });
    }
    return {
      toLoad: Object.keys(toLoad),
      pending: Object.keys(pending),
      toLoadLanguages: Object.keys(toLoadLanguages),
      toLoadNamespaces: Object.keys(toLoadNamespaces)
    };
  }
  loaded(name, err, data) {
    const s = name.split('|');
    const lng = s[0];
    const ns = s[1];
    if (err) this.emit('failedLoading', lng, ns, err);
    if (!err && data) {
      this.store.addResourceBundle(lng, ns, data, undefined, undefined, {
        skipCopy: true
      });
    }
    this.state[name] = err ? -1 : 2;
    if (err && data) this.state[name] = 0;
    const loaded = {};
    this.queue.forEach(q => {
      pushPath(q.loaded, [lng], ns);
      removePending(q, name);
      if (err) q.errors.push(err);
      if (q.pendingCount === 0 && !q.done) {
        Object.keys(q.loaded).forEach(l => {
          if (!loaded[l]) loaded[l] = {};
          const loadedKeys = q.loaded[l];
          if (loadedKeys.length) {
            loadedKeys.forEach(n => {
              if (loaded[l][n] === undefined) loaded[l][n] = true;
            });
          }
        });
        q.done = true;
        if (q.errors.length) {
          q.callback(q.errors);
        } else {
          q.callback();
        }
      }
    });
    this.emit('loaded', loaded);
    this.queue = this.queue.filter(q => !q.done);
  }
  read(lng, ns, fcName, tried = 0, wait = this.retryTimeout, callback) {
    if (!lng.length) return callback(null, {});
    if (this.readingCalls >= this.maxParallelReads) {
      this.waitingReads.push({
        lng,
        ns,
        fcName,
        tried,
        wait,
        callback
      });
      return;
    }
    this.readingCalls++;
    const resolver = (err, data) => {
      this.readingCalls--;
      if (this.waitingReads.length > 0) {
        const next = this.waitingReads.shift();
        this.read(next.lng, next.ns, next.fcName, next.tried, next.wait, next.callback);
      }
      if (err && data && tried < this.maxRetries) {
        setTimeout(() => {
          this.read.call(this, lng, ns, fcName, tried + 1, wait * 2, callback);
        }, wait);
        return;
      }
      callback(err, data);
    };
    const fc = this.backend[fcName].bind(this.backend);
    if (fc.length === 2) {
      try {
        const r = fc(lng, ns);
        if (r && typeof r.then === 'function') {
          r.then(data => resolver(null, data)).catch(resolver);
        } else {
          resolver(null, r);
        }
      } catch (err) {
        resolver(err);
      }
      return;
    }
    return fc(lng, ns, resolver);
  }
  prepareLoading(languages, namespaces, options = {}, callback) {
    if (!this.backend) {
      this.logger.warn('No backend was added via i18next.use. Will not load resources.');
      return callback && callback();
    }
    if (isString$1(languages)) languages = this.languageUtils.toResolveHierarchy(languages);
    if (isString$1(namespaces)) namespaces = [namespaces];
    const toLoad = this.queueLoad(languages, namespaces, options, callback);
    if (!toLoad.toLoad.length) {
      if (!toLoad.pending.length) callback();
      return null;
    }
    toLoad.toLoad.forEach(name => {
      this.loadOne(name);
    });
  }
  load(languages, namespaces, callback) {
    this.prepareLoading(languages, namespaces, {}, callback);
  }
  reload(languages, namespaces, callback) {
    this.prepareLoading(languages, namespaces, {
      reload: true
    }, callback);
  }
  loadOne(name, prefix = '') {
    const s = name.split('|');
    const lng = s[0];
    const ns = s[1];
    this.read(lng, ns, 'read', undefined, undefined, (err, data) => {
      if (err) this.logger.warn(`${prefix}loading namespace ${ns} for language ${lng} failed`, err);
      if (!err && data) this.logger.log(`${prefix}loaded namespace ${ns} for language ${lng}`, data);
      this.loaded(name, err, data);
    });
  }
  saveMissing(languages, namespace, key, fallbackValue, isUpdate, options = {}, clb = () => {}) {
    if (this.services?.utils?.hasLoadedNamespace && !this.services?.utils?.hasLoadedNamespace(namespace)) {
      this.logger.warn(`did not save key "${key}" as the namespace "${namespace}" was not yet loaded`, 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!');
      return;
    }
    if (key === undefined || key === null || key === '') return;
    if (this.backend?.create) {
      const opts = {
        ...options,
        isUpdate
      };
      const fc = this.backend.create.bind(this.backend);
      if (fc.length < 6) {
        try {
          let r;
          if (fc.length === 5) {
            r = fc(languages, namespace, key, fallbackValue, opts);
          } else {
            r = fc(languages, namespace, key, fallbackValue);
          }
          if (r && typeof r.then === 'function') {
            r.then(data => clb(null, data)).catch(clb);
          } else {
            clb(null, r);
          }
        } catch (err) {
          clb(err);
        }
      } else {
        fc(languages, namespace, key, fallbackValue, clb, opts);
      }
    }
    if (!languages || !languages[0]) return;
    this.store.addResource(languages[0], namespace, key, fallbackValue);
  }
}

const get = () => ({
  debug: false,
  initAsync: true,
  ns: ['translation'],
  defaultNS: ['translation'],
  fallbackLng: ['dev'],
  fallbackNS: false,
  supportedLngs: false,
  nonExplicitSupportedLngs: false,
  load: 'all',
  preload: false,
  simplifyPluralSuffix: true,
  keySeparator: '.',
  nsSeparator: ':',
  pluralSeparator: '_',
  contextSeparator: '_',
  partialBundledLanguages: false,
  saveMissing: false,
  updateMissing: false,
  saveMissingTo: 'fallback',
  saveMissingPlurals: true,
  missingKeyHandler: false,
  missingInterpolationHandler: false,
  postProcess: false,
  postProcessPassResolved: false,
  returnNull: false,
  returnEmptyString: true,
  returnObjects: false,
  joinArrays: false,
  returnedObjectHandler: false,
  parseMissingKeyHandler: false,
  appendNamespaceToMissingKey: false,
  appendNamespaceToCIMode: false,
  overloadTranslationOptionHandler: args => {
    let ret = {};
    if (typeof args[1] === 'object') ret = args[1];
    if (isString$1(args[1])) ret.defaultValue = args[1];
    if (isString$1(args[2])) ret.tDescription = args[2];
    if (typeof args[2] === 'object' || typeof args[3] === 'object') {
      const options = args[3] || args[2];
      Object.keys(options).forEach(key => {
        ret[key] = options[key];
      });
    }
    return ret;
  },
  interpolation: {
    escapeValue: true,
    format: value => value,
    prefix: '{{',
    suffix: '}}',
    formatSeparator: ',',
    unescapePrefix: '-',
    nestingPrefix: '$t(',
    nestingSuffix: ')',
    nestingOptionsSeparator: ',',
    maxReplaces: 1000,
    skipOnVariables: true
  },
  cacheInBuiltFormats: true
});
const transformOptions = options => {
  if (isString$1(options.ns)) options.ns = [options.ns];
  if (isString$1(options.fallbackLng)) options.fallbackLng = [options.fallbackLng];
  if (isString$1(options.fallbackNS)) options.fallbackNS = [options.fallbackNS];
  if (options.supportedLngs?.indexOf?.('cimode') < 0) {
    options.supportedLngs = options.supportedLngs.concat(['cimode']);
  }
  if (typeof options.initImmediate === 'boolean') options.initAsync = options.initImmediate;
  return options;
};

const noop = () => {};
const bindMemberFunctions = inst => {
  const mems = Object.getOwnPropertyNames(Object.getPrototypeOf(inst));
  mems.forEach(mem => {
    if (typeof inst[mem] === 'function') {
      inst[mem] = inst[mem].bind(inst);
    }
  });
};
class I18n extends EventEmitter {
  constructor(options = {}, callback) {
    super();
    this.options = transformOptions(options);
    this.services = {};
    this.logger = baseLogger;
    this.modules = {
      external: []
    };
    bindMemberFunctions(this);
    if (callback && !this.isInitialized && !options.isClone) {
      if (!this.options.initAsync) {
        this.init(options, callback);
        return this;
      }
      setTimeout(() => {
        this.init(options, callback);
      }, 0);
    }
  }
  init(options = {}, callback) {
    this.isInitializing = true;
    if (typeof options === 'function') {
      callback = options;
      options = {};
    }
    if (options.defaultNS == null && options.ns) {
      if (isString$1(options.ns)) {
        options.defaultNS = options.ns;
      } else if (options.ns.indexOf('translation') < 0) {
        options.defaultNS = options.ns[0];
      }
    }
    const defOpts = get();
    this.options = {
      ...defOpts,
      ...this.options,
      ...transformOptions(options)
    };
    this.options.interpolation = {
      ...defOpts.interpolation,
      ...this.options.interpolation
    };
    if (options.keySeparator !== undefined) {
      this.options.userDefinedKeySeparator = options.keySeparator;
    }
    if (options.nsSeparator !== undefined) {
      this.options.userDefinedNsSeparator = options.nsSeparator;
    }
    if (typeof this.options.overloadTranslationOptionHandler !== 'function') {
      this.options.overloadTranslationOptionHandler = defOpts.overloadTranslationOptionHandler;
    }
    const createClassOnDemand = ClassOrObject => {
      if (!ClassOrObject) return null;
      if (typeof ClassOrObject === 'function') return new ClassOrObject();
      return ClassOrObject;
    };
    if (!this.options.isClone) {
      if (this.modules.logger) {
        baseLogger.init(createClassOnDemand(this.modules.logger), this.options);
      } else {
        baseLogger.init(null, this.options);
      }
      let formatter;
      if (this.modules.formatter) {
        formatter = this.modules.formatter;
      } else {
        formatter = Formatter;
      }
      const lu = new LanguageUtil(this.options);
      this.store = new ResourceStore(this.options.resources, this.options);
      const s = this.services;
      s.logger = baseLogger;
      s.resourceStore = this.store;
      s.languageUtils = lu;
      s.pluralResolver = new PluralResolver(lu, {
        prepend: this.options.pluralSeparator,
        simplifyPluralSuffix: this.options.simplifyPluralSuffix
      });
      const usingLegacyFormatFunction = this.options.interpolation.format && this.options.interpolation.format !== defOpts.interpolation.format;
      if (usingLegacyFormatFunction) {
        this.logger.deprecate(`init: you are still using the legacy format function, please use the new approach: https://www.i18next.com/translation-function/formatting`);
      }
      if (formatter && (!this.options.interpolation.format || this.options.interpolation.format === defOpts.interpolation.format)) {
        s.formatter = createClassOnDemand(formatter);
        if (s.formatter.init) s.formatter.init(s, this.options);
        this.options.interpolation.format = s.formatter.format.bind(s.formatter);
      }
      s.interpolator = new Interpolator(this.options);
      s.utils = {
        hasLoadedNamespace: this.hasLoadedNamespace.bind(this)
      };
      s.backendConnector = new Connector(createClassOnDemand(this.modules.backend), s.resourceStore, s, this.options);
      s.backendConnector.on('*', (event, ...args) => {
        this.emit(event, ...args);
      });
      if (this.modules.languageDetector) {
        s.languageDetector = createClassOnDemand(this.modules.languageDetector);
        if (s.languageDetector.init) s.languageDetector.init(s, this.options.detection, this.options);
      }
      if (this.modules.i18nFormat) {
        s.i18nFormat = createClassOnDemand(this.modules.i18nFormat);
        if (s.i18nFormat.init) s.i18nFormat.init(this);
      }
      this.translator = new Translator(this.services, this.options);
      this.translator.on('*', (event, ...args) => {
        this.emit(event, ...args);
      });
      this.modules.external.forEach(m => {
        if (m.init) m.init(this);
      });
    }
    this.format = this.options.interpolation.format;
    if (!callback) callback = noop;
    if (this.options.fallbackLng && !this.services.languageDetector && !this.options.lng) {
      const codes = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);
      if (codes.length > 0 && codes[0] !== 'dev') this.options.lng = codes[0];
    }
    if (!this.services.languageDetector && !this.options.lng) {
      this.logger.warn('init: no languageDetector is used and no lng is defined');
    }
    const storeApi = ['getResource', 'hasResourceBundle', 'getResourceBundle', 'getDataByLanguage'];
    storeApi.forEach(fcName => {
      this[fcName] = (...args) => this.store[fcName](...args);
    });
    const storeApiChained = ['addResource', 'addResources', 'addResourceBundle', 'removeResourceBundle'];
    storeApiChained.forEach(fcName => {
      this[fcName] = (...args) => {
        this.store[fcName](...args);
        return this;
      };
    });
    const deferred = defer();
    const load = () => {
      const finish = (err, t) => {
        this.isInitializing = false;
        if (this.isInitialized && !this.initializedStoreOnce) this.logger.warn('init: i18next is already initialized. You should call init just once!');
        this.isInitialized = true;
        if (!this.options.isClone) this.logger.log('initialized', this.options);
        this.emit('initialized', this.options);
        deferred.resolve(t);
        callback(err, t);
      };
      if (this.languages && !this.isInitialized) return finish(null, this.t.bind(this));
      this.changeLanguage(this.options.lng, finish);
    };
    if (this.options.resources || !this.options.initAsync) {
      load();
    } else {
      setTimeout(load, 0);
    }
    return deferred;
  }
  loadResources(language, callback = noop) {
    let usedCallback = callback;
    const usedLng = isString$1(language) ? language : this.language;
    if (typeof language === 'function') usedCallback = language;
    if (!this.options.resources || this.options.partialBundledLanguages) {
      if (usedLng?.toLowerCase() === 'cimode' && (!this.options.preload || this.options.preload.length === 0)) return usedCallback();
      const toLoad = [];
      const append = lng => {
        if (!lng) return;
        if (lng === 'cimode') return;
        const lngs = this.services.languageUtils.toResolveHierarchy(lng);
        lngs.forEach(l => {
          if (l === 'cimode') return;
          if (toLoad.indexOf(l) < 0) toLoad.push(l);
        });
      };
      if (!usedLng) {
        const fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);
        fallbacks.forEach(l => append(l));
      } else {
        append(usedLng);
      }
      this.options.preload?.forEach?.(l => append(l));
      this.services.backendConnector.load(toLoad, this.options.ns, e => {
        if (!e && !this.resolvedLanguage && this.language) this.setResolvedLanguage(this.language);
        usedCallback(e);
      });
    } else {
      usedCallback(null);
    }
  }
  reloadResources(lngs, ns, callback) {
    const deferred = defer();
    if (typeof lngs === 'function') {
      callback = lngs;
      lngs = undefined;
    }
    if (typeof ns === 'function') {
      callback = ns;
      ns = undefined;
    }
    if (!lngs) lngs = this.languages;
    if (!ns) ns = this.options.ns;
    if (!callback) callback = noop;
    this.services.backendConnector.reload(lngs, ns, err => {
      deferred.resolve();
      callback(err);
    });
    return deferred;
  }
  use(module) {
    if (!module) throw new Error('You are passing an undefined module! Please check the object you are passing to i18next.use()');
    if (!module.type) throw new Error('You are passing a wrong module! Please check the object you are passing to i18next.use()');
    if (module.type === 'backend') {
      this.modules.backend = module;
    }
    if (module.type === 'logger' || module.log && module.warn && module.error) {
      this.modules.logger = module;
    }
    if (module.type === 'languageDetector') {
      this.modules.languageDetector = module;
    }
    if (module.type === 'i18nFormat') {
      this.modules.i18nFormat = module;
    }
    if (module.type === 'postProcessor') {
      postProcessor.addPostProcessor(module);
    }
    if (module.type === 'formatter') {
      this.modules.formatter = module;
    }
    if (module.type === '3rdParty') {
      this.modules.external.push(module);
    }
    return this;
  }
  setResolvedLanguage(l) {
    if (!l || !this.languages) return;
    if (['cimode', 'dev'].indexOf(l) > -1) return;
    for (let li = 0; li < this.languages.length; li++) {
      const lngInLngs = this.languages[li];
      if (['cimode', 'dev'].indexOf(lngInLngs) > -1) continue;
      if (this.store.hasLanguageSomeTranslations(lngInLngs)) {
        this.resolvedLanguage = lngInLngs;
        break;
      }
    }
    if (!this.resolvedLanguage && this.languages.indexOf(l) < 0 && this.store.hasLanguageSomeTranslations(l)) {
      this.resolvedLanguage = l;
      this.languages.unshift(l);
    }
  }
  changeLanguage(lng, callback) {
    this.isLanguageChangingTo = lng;
    const deferred = defer();
    this.emit('languageChanging', lng);
    const setLngProps = l => {
      this.language = l;
      this.languages = this.services.languageUtils.toResolveHierarchy(l);
      this.resolvedLanguage = undefined;
      this.setResolvedLanguage(l);
    };
    const done = (err, l) => {
      if (l) {
        if (this.isLanguageChangingTo === lng) {
          setLngProps(l);
          this.translator.changeLanguage(l);
          this.isLanguageChangingTo = undefined;
          this.emit('languageChanged', l);
          this.logger.log('languageChanged', l);
        }
      } else {
        this.isLanguageChangingTo = undefined;
      }
      deferred.resolve((...args) => this.t(...args));
      if (callback) callback(err, (...args) => this.t(...args));
    };
    const setLng = lngs => {
      if (!lng && !lngs && this.services.languageDetector) lngs = [];
      const fl = isString$1(lngs) ? lngs : lngs && lngs[0];
      const l = this.store.hasLanguageSomeTranslations(fl) ? fl : this.services.languageUtils.getBestMatchFromCodes(isString$1(lngs) ? [lngs] : lngs);
      if (l) {
        if (!this.language) {
          setLngProps(l);
        }
        if (!this.translator.language) this.translator.changeLanguage(l);
        this.services.languageDetector?.cacheUserLanguage?.(l);
      }
      this.loadResources(l, err => {
        done(err, l);
      });
    };
    if (!lng && this.services.languageDetector && !this.services.languageDetector.async) {
      setLng(this.services.languageDetector.detect());
    } else if (!lng && this.services.languageDetector && this.services.languageDetector.async) {
      if (this.services.languageDetector.detect.length === 0) {
        this.services.languageDetector.detect().then(setLng);
      } else {
        this.services.languageDetector.detect(setLng);
      }
    } else {
      setLng(lng);
    }
    return deferred;
  }
  getFixedT(lng, ns, keyPrefix) {
    const fixedT = (key, opts, ...rest) => {
      let o;
      if (typeof opts !== 'object') {
        o = this.options.overloadTranslationOptionHandler([key, opts].concat(rest));
      } else {
        o = {
          ...opts
        };
      }
      o.lng = o.lng || fixedT.lng;
      o.lngs = o.lngs || fixedT.lngs;
      o.ns = o.ns || fixedT.ns;
      if (o.keyPrefix !== '') o.keyPrefix = o.keyPrefix || keyPrefix || fixedT.keyPrefix;
      const keySeparator = this.options.keySeparator || '.';
      let resultKey;
      if (o.keyPrefix && Array.isArray(key)) {
        resultKey = key.map(k => {
          if (typeof k === 'function') k = keysFromSelector(k, {
            ...this.options,
            ...opts
          });
          return `${o.keyPrefix}${keySeparator}${k}`;
        });
      } else {
        if (typeof key === 'function') key = keysFromSelector(key, {
          ...this.options,
          ...opts
        });
        resultKey = o.keyPrefix ? `${o.keyPrefix}${keySeparator}${key}` : key;
      }
      return this.t(resultKey, o);
    };
    if (isString$1(lng)) {
      fixedT.lng = lng;
    } else {
      fixedT.lngs = lng;
    }
    fixedT.ns = ns;
    fixedT.keyPrefix = keyPrefix;
    return fixedT;
  }
  t(...args) {
    return this.translator?.translate(...args);
  }
  exists(...args) {
    return this.translator?.exists(...args);
  }
  setDefaultNamespace(ns) {
    this.options.defaultNS = ns;
  }
  hasLoadedNamespace(ns, options = {}) {
    if (!this.isInitialized) {
      this.logger.warn('hasLoadedNamespace: i18next was not initialized', this.languages);
      return false;
    }
    if (!this.languages || !this.languages.length) {
      this.logger.warn('hasLoadedNamespace: i18n.languages were undefined or empty', this.languages);
      return false;
    }
    const lng = options.lng || this.resolvedLanguage || this.languages[0];
    const fallbackLng = this.options ? this.options.fallbackLng : false;
    const lastLng = this.languages[this.languages.length - 1];
    if (lng.toLowerCase() === 'cimode') return true;
    const loadNotPending = (l, n) => {
      const loadState = this.services.backendConnector.state[`${l}|${n}`];
      return loadState === -1 || loadState === 0 || loadState === 2;
    };
    if (options.precheck) {
      const preResult = options.precheck(this, loadNotPending);
      if (preResult !== undefined) return preResult;
    }
    if (this.hasResourceBundle(lng, ns)) return true;
    if (!this.services.backendConnector.backend || this.options.resources && !this.options.partialBundledLanguages) return true;
    if (loadNotPending(lng, ns) && (!fallbackLng || loadNotPending(lastLng, ns))) return true;
    return false;
  }
  loadNamespaces(ns, callback) {
    const deferred = defer();
    if (!this.options.ns) {
      if (callback) callback();
      return Promise.resolve();
    }
    if (isString$1(ns)) ns = [ns];
    ns.forEach(n => {
      if (this.options.ns.indexOf(n) < 0) this.options.ns.push(n);
    });
    this.loadResources(err => {
      deferred.resolve();
      if (callback) callback(err);
    });
    return deferred;
  }
  loadLanguages(lngs, callback) {
    const deferred = defer();
    if (isString$1(lngs)) lngs = [lngs];
    const preloaded = this.options.preload || [];
    const newLngs = lngs.filter(lng => preloaded.indexOf(lng) < 0 && this.services.languageUtils.isSupportedCode(lng));
    if (!newLngs.length) {
      if (callback) callback();
      return Promise.resolve();
    }
    this.options.preload = preloaded.concat(newLngs);
    this.loadResources(err => {
      deferred.resolve();
      if (callback) callback(err);
    });
    return deferred;
  }
  dir(lng) {
    if (!lng) lng = this.resolvedLanguage || (this.languages?.length > 0 ? this.languages[0] : this.language);
    if (!lng) return 'rtl';
    try {
      const l = new Intl.Locale(lng);
      if (l && l.getTextInfo) {
        const ti = l.getTextInfo();
        if (ti && ti.direction) return ti.direction;
      }
    } catch (e) {}
    const rtlLngs = ['ar', 'shu', 'sqr', 'ssh', 'xaa', 'yhd', 'yud', 'aao', 'abh', 'abv', 'acm', 'acq', 'acw', 'acx', 'acy', 'adf', 'ads', 'aeb', 'aec', 'afb', 'ajp', 'apc', 'apd', 'arb', 'arq', 'ars', 'ary', 'arz', 'auz', 'avl', 'ayh', 'ayl', 'ayn', 'ayp', 'bbz', 'pga', 'he', 'iw', 'ps', 'pbt', 'pbu', 'pst', 'prp', 'prd', 'ug', 'ur', 'ydd', 'yds', 'yih', 'ji', 'yi', 'hbo', 'men', 'xmn', 'fa', 'jpr', 'peo', 'pes', 'prs', 'dv', 'sam', 'ckb'];
    const languageUtils = this.services?.languageUtils || new LanguageUtil(get());
    if (lng.toLowerCase().indexOf('-latn') > 1) return 'ltr';
    return rtlLngs.indexOf(languageUtils.getLanguagePartFromCode(lng)) > -1 || lng.toLowerCase().indexOf('-arab') > 1 ? 'rtl' : 'ltr';
  }
  static createInstance(options = {}, callback) {
    const instance = new I18n(options, callback);
    instance.createInstance = I18n.createInstance;
    return instance;
  }
  cloneInstance(options = {}, callback = noop) {
    const forkResourceStore = options.forkResourceStore;
    if (forkResourceStore) delete options.forkResourceStore;
    const mergedOptions = {
      ...this.options,
      ...options,
      ...{
        isClone: true
      }
    };
    const clone = new I18n(mergedOptions);
    if (options.debug !== undefined || options.prefix !== undefined) {
      clone.logger = clone.logger.clone(options);
    }
    const membersToCopy = ['store', 'services', 'language'];
    membersToCopy.forEach(m => {
      clone[m] = this[m];
    });
    clone.services = {
      ...this.services
    };
    clone.services.utils = {
      hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone)
    };
    if (forkResourceStore) {
      const clonedData = Object.keys(this.store.data).reduce((prev, l) => {
        prev[l] = {
          ...this.store.data[l]
        };
        prev[l] = Object.keys(prev[l]).reduce((acc, n) => {
          acc[n] = {
            ...prev[l][n]
          };
          return acc;
        }, prev[l]);
        return prev;
      }, {});
      clone.store = new ResourceStore(clonedData, mergedOptions);
      clone.services.resourceStore = clone.store;
    }
    if (options.interpolation) {
      const defOpts = get();
      const mergedInterpolation = {
        ...defOpts.interpolation,
        ...this.options.interpolation,
        ...options.interpolation
      };
      const mergedForInterpolator = {
        ...mergedOptions,
        interpolation: mergedInterpolation
      };
      clone.services.interpolator = new Interpolator(mergedForInterpolator);
    }
    clone.translator = new Translator(clone.services, mergedOptions);
    clone.translator.on('*', (event, ...args) => {
      clone.emit(event, ...args);
    });
    clone.init(mergedOptions, callback);
    clone.translator.options = mergedOptions;
    clone.translator.backendConnector.services.utils = {
      hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone)
    };
    return clone;
  }
  toJSON() {
    return {
      options: this.options,
      store: this.store,
      language: this.language,
      languages: this.languages,
      resolvedLanguage: this.resolvedLanguage
    };
  }
}
const instance = I18n.createInstance();

const warn = (i18n, code, msg, rest) => {
  const args = [msg, {
    code,
    ...(rest || {})
  }];
  if (i18n?.services?.logger?.forward) {
    return i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
  }
  if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
  if (i18n?.services?.logger?.warn) {
    i18n.services.logger.warn(...args);
  } else if (console?.warn) {
    console.warn(...args);
  }
};
const alreadyWarned = {};
const warnOnce$1 = (i18n, code, msg, rest) => {
  if (isString(msg) && alreadyWarned[msg]) return;
  if (isString(msg)) alreadyWarned[msg] = new Date();
  warn(i18n, code, msg, rest);
};
const loadedClb = (i18n, cb) => () => {
  if (i18n.isInitialized) {
    cb();
  } else {
    const initialized = () => {
      setTimeout(() => {
        i18n.off('initialized', initialized);
      }, 0);
      cb();
    };
    i18n.on('initialized', initialized);
  }
};
const loadNamespaces = (i18n, ns, cb) => {
  i18n.loadNamespaces(ns, loadedClb(i18n, cb));
};
const loadLanguages = (i18n, lng, ns, cb) => {
  if (isString(ns)) ns = [ns];
  if (i18n.options.preload && i18n.options.preload.indexOf(lng) > -1) return loadNamespaces(i18n, ns, cb);
  ns.forEach(n => {
    if (i18n.options.ns.indexOf(n) < 0) i18n.options.ns.push(n);
  });
  i18n.loadLanguages(lng, loadedClb(i18n, cb));
};
const hasLoadedNamespace = (ns, i18n, options = {}) => {
  if (!i18n.languages || !i18n.languages.length) {
    warnOnce$1(i18n, 'NO_LANGUAGES', 'i18n.languages were undefined or empty', {
      languages: i18n.languages
    });
    return true;
  }
  return i18n.hasLoadedNamespace(ns, {
    lng: options.lng,
    precheck: (i18nInstance, loadNotPending) => {
      if (options.bindI18n && options.bindI18n.indexOf('languageChanging') > -1 && i18nInstance.services.backendConnector.backend && i18nInstance.isLanguageChangingTo && !loadNotPending(i18nInstance.isLanguageChangingTo, ns)) return false;
    }
  });
};
const isString = obj => typeof obj === 'string';
const isObject = obj => typeof obj === 'object' && obj !== null;

const matchHtmlEntity = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g;
const htmlEntities = {
  '&amp;': '&',
  '&#38;': '&',
  '&lt;': '<',
  '&#60;': '<',
  '&gt;': '>',
  '&#62;': '>',
  '&apos;': "'",
  '&#39;': "'",
  '&quot;': '"',
  '&#34;': '"',
  '&nbsp;': ' ',
  '&#160;': ' ',
  '&copy;': '©',
  '&#169;': '©',
  '&reg;': '®',
  '&#174;': '®',
  '&hellip;': '…',
  '&#8230;': '…',
  '&#x2F;': '/',
  '&#47;': '/'
};
const unescapeHtmlEntity = m => htmlEntities[m];
const unescape = text => text.replace(matchHtmlEntity, unescapeHtmlEntity);

let defaultOptions = {
  bindI18n: 'languageChanged',
  bindI18nStore: '',
  transEmptyNodeValue: '',
  transSupportBasicHtmlNodes: true,
  transWrapTextNodes: '',
  transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
  useSuspense: true,
  unescape,
  transDefaultProps: undefined
};
const setDefaults = (options = {}) => {
  defaultOptions = {
    ...defaultOptions,
    ...options
  };
};
const getDefaults = () => defaultOptions;

let i18nInstance;
const setI18n = instance => {
  i18nInstance = instance;
};
const getI18n = () => i18nInstance;

const initReactI18next = {
  type: '3rdParty',
  init(instance) {
    setDefaults(instance.options.react);
    setI18n(instance);
  }
};

const I18nContext = SP_REACT.createContext();
class ReportNamespaces {
  constructor() {
    this.usedNamespaces = {};
  }
  addUsedNamespaces(namespaces) {
    namespaces.forEach(ns => {
      if (!this.usedNamespaces[ns]) this.usedNamespaces[ns] = true;
    });
  }
  getUsedNamespaces() {
    return Object.keys(this.usedNamespaces);
  }
}

var shim = {exports: {}};

var useSyncExternalStoreShim_production = {};

const _global_SP_REACT = SP_REACT;

/**
 * @license React
 * use-sync-external-store-shim.production.js
 *
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

var hasRequiredUseSyncExternalStoreShim_production;

function requireUseSyncExternalStoreShim_production () {
	if (hasRequiredUseSyncExternalStoreShim_production) return useSyncExternalStoreShim_production;
	hasRequiredUseSyncExternalStoreShim_production = 1;
	var React = _global_SP_REACT;
	function is(x, y) {
	  return (x === y && (0 !== x || 1 / x === 1 / y)) || (x !== x && y !== y);
	}
	var objectIs = "function" === typeof Object.is ? Object.is : is,
	  useState = React.useState,
	  useEffect = React.useEffect,
	  useLayoutEffect = React.useLayoutEffect,
	  useDebugValue = React.useDebugValue;
	function useSyncExternalStore$2(subscribe, getSnapshot) {
	  var value = getSnapshot(),
	    _useState = useState({ inst: { value: value, getSnapshot: getSnapshot } }),
	    inst = _useState[0].inst,
	    forceUpdate = _useState[1];
	  useLayoutEffect(
	    function () {
	      inst.value = value;
	      inst.getSnapshot = getSnapshot;
	      checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });
	    },
	    [subscribe, value, getSnapshot]
	  );
	  useEffect(
	    function () {
	      checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });
	      return subscribe(function () {
	        checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });
	      });
	    },
	    [subscribe]
	  );
	  useDebugValue(value);
	  return value;
	}
	function checkIfSnapshotChanged(inst) {
	  var latestGetSnapshot = inst.getSnapshot;
	  inst = inst.value;
	  try {
	    var nextValue = latestGetSnapshot();
	    return !objectIs(inst, nextValue);
	  } catch (error) {
	    return true;
	  }
	}
	function useSyncExternalStore$1(subscribe, getSnapshot) {
	  return getSnapshot();
	}
	var shim =
	  "undefined" === typeof window ||
	  "undefined" === typeof window.document ||
	  "undefined" === typeof window.document.createElement
	    ? useSyncExternalStore$1
	    : useSyncExternalStore$2;
	useSyncExternalStoreShim_production.useSyncExternalStore =
	  void 0 !== React.useSyncExternalStore ? React.useSyncExternalStore : shim;
	return useSyncExternalStoreShim_production;
}

{
  shim.exports = requireUseSyncExternalStoreShim_production();
}

var shimExports = shim.exports;

const notReadyT = (k, optsOrDefaultValue) => {
  if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
  if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue)) return optsOrDefaultValue.defaultValue;
  return Array.isArray(k) ? k[k.length - 1] : k;
};
const notReadySnapshot = {
  t: notReadyT,
  ready: false
};
const dummySubscribe = () => () => {};
const useTranslation = (ns, props = {}) => {
  const {
    i18n: i18nFromProps
  } = props;
  const {
    i18n: i18nFromContext,
    defaultNS: defaultNSFromContext
  } = SP_REACT.useContext(I18nContext) || {};
  const i18n = i18nFromProps || i18nFromContext || getI18n();
  if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
  if (!i18n) {
    warnOnce$1(i18n, 'NO_I18NEXT_INSTANCE', 'useTranslation: You will need to pass in an i18next instance by using initReactI18next');
  }
  const i18nOptions = SP_REACT.useMemo(() => ({
    ...getDefaults(),
    ...i18n?.options?.react,
    ...props
  }), [i18n, props]);
  const {
    useSuspense,
    keyPrefix
  } = i18nOptions;
  const nsOrContext = defaultNSFromContext || i18n?.options?.defaultNS;
  const unstableNamespaces = isString(nsOrContext) ? [nsOrContext] : nsOrContext || ['translation'];
  const namespaces = SP_REACT.useMemo(() => unstableNamespaces, unstableNamespaces);
  i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces);
  const revisionRef = SP_REACT.useRef(0);
  const subscribe = SP_REACT.useCallback(callback => {
    if (!i18n) return dummySubscribe;
    const {
      bindI18n,
      bindI18nStore
    } = i18nOptions;
    const wrappedCallback = () => {
      revisionRef.current += 1;
      callback();
    };
    if (bindI18n) i18n.on(bindI18n, wrappedCallback);
    if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback);
    return () => {
      if (bindI18n) bindI18n.split(' ').forEach(e => i18n.off(e, wrappedCallback));
      if (bindI18nStore) bindI18nStore.split(' ').forEach(e => i18n.store.off(e, wrappedCallback));
    };
  }, [i18n, i18nOptions]);
  const snapshotRef = SP_REACT.useRef();
  const getSnapshot = SP_REACT.useCallback(() => {
    if (!i18n) {
      return notReadySnapshot;
    }
    const calculatedReady = !!(i18n.isInitialized || i18n.initializedStoreOnce) && namespaces.every(n => hasLoadedNamespace(n, i18n, i18nOptions));
    const currentLng = props.lng || i18n.language;
    const currentRevision = revisionRef.current;
    const lastSnapshot = snapshotRef.current;
    if (lastSnapshot && lastSnapshot.ready === calculatedReady && lastSnapshot.lng === currentLng && lastSnapshot.keyPrefix === keyPrefix && lastSnapshot.revision === currentRevision) {
      return lastSnapshot;
    }
    const calculatedT = i18n.getFixedT(currentLng, i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0], keyPrefix);
    const newSnapshot = {
      t: calculatedT,
      ready: calculatedReady,
      lng: currentLng,
      keyPrefix,
      revision: currentRevision
    };
    snapshotRef.current = newSnapshot;
    return newSnapshot;
  }, [i18n, namespaces, keyPrefix, i18nOptions, props.lng]);
  const [loadCount, setLoadCount] = SP_REACT.useState(0);
  const {
    t,
    ready
  } = shimExports.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
  SP_REACT.useEffect(() => {
    if (i18n && !ready && !useSuspense) {
      const onLoaded = () => setLoadCount(c => c + 1);
      if (props.lng) {
        loadLanguages(i18n, props.lng, namespaces, onLoaded);
      } else {
        loadNamespaces(i18n, namespaces, onLoaded);
      }
    }
  }, [i18n, props.lng, namespaces, ready, useSuspense, loadCount]);
  const finalI18n = i18n || {};
  const wrapperRef = SP_REACT.useRef(null);
  const wrapperLangRef = SP_REACT.useRef();
  const createI18nWrapper = original => {
    const descriptors = Object.getOwnPropertyDescriptors(original);
    if (descriptors.__original) delete descriptors.__original;
    const wrapper = Object.create(Object.getPrototypeOf(original), descriptors);
    if (!Object.prototype.hasOwnProperty.call(wrapper, '__original')) {
      try {
        Object.defineProperty(wrapper, '__original', {
          value: original,
          writable: false,
          enumerable: false,
          configurable: false
        });
      } catch (_) {}
    }
    return wrapper;
  };
  const ret = SP_REACT.useMemo(() => {
    const original = finalI18n;
    const lang = original?.language;
    let i18nWrapper = original;
    if (original) {
      if (wrapperRef.current && wrapperRef.current.__original === original) {
        if (wrapperLangRef.current !== lang) {
          i18nWrapper = createI18nWrapper(original);
          wrapperRef.current = i18nWrapper;
          wrapperLangRef.current = lang;
        } else {
          i18nWrapper = wrapperRef.current;
        }
      } else {
        i18nWrapper = createI18nWrapper(original);
        wrapperRef.current = i18nWrapper;
        wrapperLangRef.current = lang;
      }
    }
    const arr = [t, i18nWrapper, ready];
    arr.t = t;
    arr.i18n = i18nWrapper;
    arr.ready = ready;
    return arr;
  }, [t, finalI18n, ready, finalI18n.resolvedLanguage, finalI18n.language, finalI18n.languages]);
  if (i18n && useSuspense && !ready) {
    throw new Promise(resolve => {
      const onLoaded = () => resolve();
      if (props.lng) {
        loadLanguages(i18n, props.lng, namespaces, onLoaded);
      } else {
        loadNamespaces(i18n, namespaces, onLoaded);
      }
    });
  }
  return ret;
};

var _comment$f = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$f = {
	settings: "Settings",
	downloads: "Downloads"
};
var auth$f = {
	connect: "Connect",
	disconnect: "Disconnect",
	status: {
		connected: "Connected",
		disconnected: "Not connected",
		checking: "Checking…",
		error: "Error — check logs"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$f = {
	title: "Storage",
	defaultUpdated: "Default storage updated",
	isDefault: "Default",
	setDefault: "Set Default",
	customPath: "Custom Path",
	up: "↑ Up",
	refresh: "Refresh",
	useThisPath: "Use This Path"
};
var storageSettings$f = {
	title: "Download Settings",
	installLocationLabel: "Install Location",
	installLocationDescription: "Where new games will be downloaded",
	pathLabel: "Path",
	loading: "Loading storage options...",
	toastUpdatedTitle: "Storage Location Updated",
	toastUpdatedBody: "New games will be installed to {{location}}",
	toastFailedTitle: "Failed to Update",
	toastFailedBody: "{{error}}",
	unknown: "Unknown",
	browseButton: "Browse...",
	clearCustom: "Clear Custom Location",
	customPathSet: "Custom install location set",
	newFolder: "New Folder",
	newFolderCreate: "Create",
	selectFolder: "Use This Folder",
	emptyDirectory: "Empty folder",
	showHidden: "Show Hidden",
	selectDevice: "Select Device",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Modified (Newest)",
	sortModifiedOldest: "Modified (Oldest)",
	sortCreatedNewest: "Created (Newest)",
	sortCreatedOldest: "Created (Oldest)",
	sortSizeLargest: "Size (Largest)",
	sortSizeSmallest: "Size (Smallest)",
	"validationErrors.notAbsolute": "Path must be absolute",
	"validationErrors.systemDir": "Cannot use a system directory",
	"validationErrors.notWritable": "Location is not writable",
	"validationErrors.notFound": "Directory does not exist",
	"validationErrors.homeRoot": "Cannot use home directory root",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB free)"
};
var gameGrid$f = {
	loading: "Loading games...",
	noGamesFound: "No games found",
	noGamesHint: "Try syncing your libraries from the Unifideck settings",
	showingGames: "Showing {{count}} games",
	installed: "Installed",
	notInstalled: "Not Installed",
	playtime: "{{hours}}h {{minutes}}m played"
};
var downloadsTab$f = {
	currentDownload: "CURRENT DOWNLOAD",
	queuedDownloads: "QUEUED ({{count}})",
	recentlyCompleted: "RECENTLY COMPLETED",
	noActiveDownloads: "No active downloads",
	noDownloads: "No downloads. Install games from your library to see them here.",
	loadingLabel: "Loading...",
	loadingMessage: "Fetching download queue...",
	confirmCancelTitle: "Confirm Cancellation",
	confirmCancelDescription: "Are you sure you want to cancel the download for {{game}}?",
	confirmCancelYes: "Yes",
	confirmCancelNo: "No",
	cancelDownload: "Cancel",
	toastDownloadCancelledTitle: "Download Cancelled",
	toastDownloadCancelledBody: "The download has been removed from the queue.",
	toastCancelFailedTitle: "Cancel Failed",
	toastCancelFailedBody: "Unable to cancel download: {{error}}",
	status: {
		queued: "Queued",
		downloading: "Downloading",
		completed: "Completed",
		cancelled: "Cancelled",
		error: "Error"
	},
	preparingDownload: "Preparing download...",
	phaseExtracting: "Extracting {{game}}...",
	phaseVerifying: "Verifying {{game}}...",
	internalStorage: "Internal Storage ({{freeSpace}} GB free)",
	sdCard: "SD Card ({{freeSpace}} GB free)",
	customLocation: "Custom: {{path}} ({{freeSpace}} GB free)",
	extractingLabel: "Extracting...",
	verifyingLabel: "Verifying...",
	installingViaUpcLabel: "Installing Ubisoft Connect",
	preparingLabel: "Setting up game...",
	preparingMessage: "Initializing prefix and syncing cloud saves...",
	extractingMessage: "Extracting files...",
	verifyingMessage: "Verifying files... {{pct}}%",
	installingViaUpcMessage: "Follow the Ubisoft Connect window to finish installing...",
	updateQueuedLabel: "Update Queued",
	downloadQueuedLabel: "Download Queued",
	downloadingUpdateLabel: "Downloading Update",
	downloadingLabel: "Downloading...",
	finalizingInstallation: "Finalizing installation...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "ETA: {{eta}}"
};
var cloudSave$f = {
	title: "Cloud Save Conflict",
	description: "Your local saves differ from cloud saves for {{game}}",
	local: "Local:",
	cloud: "Cloud:",
	newer: "(newer)",
	filesSize: "{{count}} files · {{size}}",
	uploadBlockedEmpty: "Cloud upload skipped for {{game}} — no local saves found",
	uploadSkippedTitle: "Cloud upload skipped",
	uploadSkippedBody: "No local saves to upload yet for {{game}}.",
	useCloud: "Use Cloud",
	useLocal: "Use Local",
	unknownDate: "Unknown",
	toastUsingCloudTitle: "Using Cloud Saves",
	toastUsingCloudBody: "Downloading cloud saves for {{game}}",
	toastUsingLocalTitle: "Using Local Saves",
	toastUsingLocalBody: "Uploading local saves for {{game}}",
	toastErrorTitle: "Error",
	toastErrorBody: "Failed to resolve conflict"
};
var storeConnections$f = {
	title: "STORE CONNECTIONS",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Connected",
	checking: "Checking...",
	authenticate: "Sign in",
	legendaryNotInstalled: "⚠️ Installing Epic Games CLI...",
	nileNotInstalled: "⚠️ Amazon Games CLI not found",
	error: "Error - Check logs"
};
var librarySync$f = {
	title: "LIBRARY SYNC",
	syncLibraries: "Sync Libraries",
	forceSync: "Force Sync",
	syncing: "Syncing...",
	cancelSync: "Cancel Sync",
	retryStatusCheck: "Retry Status Check",
	gamesSynced: "{{synced}} / {{total}} games synced",
	artworkDownloaded: "{{synced}} / {{total}} artwork covers downloaded",
	metadataExtracted: "{{synced}} / {{total}} metadata records extracted",
	steamMetadataDownloaded: "Steam metadata: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDB metadata: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacritic data: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWG metadata: {{synced}} / {{total}}",
	compatChecked: "Compatibility data: {{synced}} / {{total}}",
	cancelling: "Cancelling…",
	error: "Error"
};
var cleanup$f = {
	title: "Cleanup",
	deleteAll: "Delete all UNIFIDECK Libraries and Cache",
	modalTitle: "Delete all Unifideck data?",
	modalDescription: "This will remove every Unifideck shortcut, Steam artwork, saved store login, and cached data. Installed games stay on disk and can be re-synced. Enable destructive mode below to also delete the installed game files.",
	deleteFilesLabel: "Also delete installed game files (Destructive)",
	deleteFilesDescription: "Removes the on-disk install directory for every Unifideck game under a safe root.",
	confirmDelete: "Yes, Delete Everything",
	cancel: "Cancel",
	deleting: "Deleting..."
};
var captureLogs$f = {
	_comment: "CaptureLogsSection (QAM Settings tab, rendered directly below CleanupSection) + CaptureLogsConfirmModal. One button calls the capture_logs RPC, which zips every log, state file and diagnostic report into the user's Downloads folder so it can be attached to a bug report. The returned path is kept on screen after the toast expires, rendered dir=ltr with word-break so a long POSIX path stays readable in a ~300px row. partialNote is genuinely pluralised via {{count}}, so both forms must exist; bundleInfo uses {{files}} instead to stay out of i18next's plural machinery.",
	title: "Capture Logs",
	description: "Collects every Unifideck log into a single zip in your Downloads folder, so you can attach it to a bug report.",
	capture: "Capture logs",
	capturing: "Collecting logs...",
	savedTo: "Saved to your Downloads folder",
	bundleInfo: "{{files}} files · {{size}}",
	partialNote: "{{count}} file could not be read. The bundle is still usable.",
	partialNote_other: "{{count}} files could not be read. The bundle is still usable.",
	copyPath: "Copy path",
	modalTitle: "Create a diagnostics bundle?",
	modalDescription: "Unifideck will collect its logs into a single zip file in your Downloads folder, so you can attach it to a bug report.",
	modalIncludes: "Included: plugin and game launch logs, sync and install history, your game titles and install paths, and details about your device such as your model, operating system and storage.",
	modalExcludes: "Never included: your passwords, store login tokens, or browser cookies.",
	modalConfirm: "Create bundle",
	cancel: "Cancel"
};
var installButton$f = {
	play: "Play",
	resume: "Resume",
	install: "Install {{title}}",
	installNative: "Install",
	uninstall: "Uninstall {{title}}",
	cancel: "✖ Cancel",
	processing: "Processing...",
	checking: "Checking...",
	size: "{{size}} GB",
	update: "UPDATE",
	updateAvailable: "UPDATE AVAILABLE",
	updateAvailableDescription: "A new version is ready to install."
};
var confirmModals$f = {
	installTitle: "Confirm Installation",
	installDescription: "Are you sure you want to install {{title}}?",
	uninstallTitle: "Confirm Uninstallation",
	uninstallDescription: "Are you sure you want to uninstall {{title}}?",
	cancelTitle: "Confirm Cancellation",
	cancelDescription: "Are you sure you want to cancel the download for {{title}}?",
	forceSyncTitle: "Force Sync Settings",
	forceSyncDescription: "Would you like to resync all artwork? This will overwrite any manual artwork changes. Select 'Keep Artwork' to only download missing artwork.",
	resyncArtwork: "Resync Artwork",
	keepArtwork: "Keep Artwork",
	steamRestartTitle: "Sync Complete!",
	steamRestartDescription: "Please Restart Steam to apply your new games and artwork",
	steamRestartDescriptionStore: "Please Restart Steam to apply your new {{store}} games and artwork",
	steamRestartCleanupTitle: "Cleanup Complete!",
	steamRestartCleanupDescription: "Please Restart Steam to fully remove deleted games and artwork",
	restartNow: "Restart Now",
	later: "Later",
	ubisoftInstallTitle: "Ubisoft Connect Required",
	ubisoftInstallDescription: "This will load the Ubisoft Connect Launcher which you will need to install {{title}}.",
	ubisoftInstallConfirm: "Open Ubisoft Connect",
	yes: "Yes",
	no: "No",
	amazonUpdateTitle: "Full Re-download Required",
	amazonUpdateDescription: "Amazon updates require a full re-download of {{title}}. This may take a while. Continue?",
	updateConfirm: "Update"
};
var toasts$f = {
	downloadStarted: "Download Started. Check UNIFIDECK > Downloads",
	downloadQueued: "Download queued",
	downloadQueuedBody: "{{title}} has been added to the queue.",
	downloadFailedUnknown: "Unknown error.",
	updateQueued: "Update queued",
	uninstallDone: "Uninstall complete.",
	cloudPullStarted: "Downloading cloud save…",
	cloudPullDone: "Cloud save downloaded",
	cloudPullFailed: "Cloud save download failed",
	cloudPushStarted: "Uploading local save…",
	cloudPushDone: "Local save uploaded",
	cloudPushFailed: "Local save upload failed",
	cloudSavePathSet: "Save location updated",
	cloudSavePathFailed: "Couldn't set save location",
	ubisoftLauncherOpening: "Opening Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect is opening for {{title}}. Install the game there, then return to play.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect is already open for this install.",
	ubisoftLauncherOpenFailed: "Could Not Open Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Failed to open Ubisoft Connect for installation.",
	multipartDetected: "Multi-Part Download Detected",
	multipartMessage: "Please be patient and wait for completion",
	downloadFailed: "Download Failed",
	downloadFailedMessage: "Could not start download.",
	downloadCancelled: "Download Cancelled",
	downloadCancelledMessage: "{{title}} download cancelled.",
	cancelFailed: "Cancel Failed",
	cancelFailedMessage: "Could not cancel download.",
	uninstalling: "Uninstalling...",
	uninstallingMessage: "Uninstalling {{title}}...",
	uninstallingMessageProton: "Uninstalling {{title}} and Proton files...",
	uninstallComplete: "Uninstall Complete",
	uninstallCompleteMessage: "{{title}} removed.",
	uninstallCompleteMessageProton: "{{title}} removed (including Proton files).",
	installComplete: "Installation Complete!",
	installCompleteMessage: "{{title}} is ready to play.",
	syncComplete: "Sync Complete!",
	syncCompleteMessage: "Added {{count}} games. RESTART STEAM (exit completely, not just return to game mode) to see them in your library.",
	forceSyncComplete: "Force Sync Complete!",
	forceSyncCompleteMessage: "Updated {{count}} games. RESTART STEAM to see changes.",
	syncCancelled: "Sync Cancelled",
	authTimeout: "Authentication Timeout",
	authTimeoutMessage: "{{store}} auth timed out. Please try again.",
	authFailed: "Authentication Failed",
	authFailedMessage: "Failed to start authentication",
	authError: "Authentication Error",
	authConnected: "{{store}} Connected!",
	authConnectedMessage: "Successfully authenticated with {{store}}",
	cleanupStarted: "Initiating full reset for Unifideck",
	cleanupStartedMessage: "Removing shortcuts, artwork, saved logins, prefixes, and cached data. This may take a moment…",
	cleanupSuccessful: "Cleanup Successful",
	cleanupSuccessfulMessage: "Removed {{games}} shortcuts, {{artwork}} artwork files, signed out of {{stores}} stores, deleted {{files}} install dirs, wiped {{residual}} data files. Cache cleared.",
	cleanupNoop: "Nothing to Clean",
	cleanupNoopMessage: "Unifideck has no shortcuts, artwork, or saved logins to remove.",
	deleteFailed: "Delete Failed",
	repairPrefixSuccess: "Prefix Repaired",
	repairPrefixSuccessMessage: "Ubisoft Connect prefix has been rebuilt",
	repairPrefixFailed: "Repair Failed",
	repairPrefixFailedMessage: "Could not repair prefix: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Network Error",
		launchRetry: "Retrying Launch",
		retryingUmu: "Retrying UMU in {{seconds}}s (attempt {{attempt}}/{{max}})...",
		protonUpgrade: "Proton Upgrade",
		resettingPrefix: "Resetting prefix for {{version}}...",
		firstTimeSetup: "First Time Setup",
		initializingPrefix: "Initializing Wine prefix...",
		downloadingRuntime: "Downloading Steam Runtime — first launch may take a few minutes...",
		setupCompleteTitle: "Setup Complete",
		prefixInitialized: "Wine prefix initialized",
		setupFallback: "Setup",
		fallbackInitialization: "Fallback initialization...",
		launchingGame: "Launching Game",
		startingEpicGame: "Starting Epic game...",
		startingAmazonGame: "Starting Amazon game...",
		startingGogGame: "Starting GOG game...",
		startingUbisoftGame: "Starting Ubisoft game...",
		signingInUbisoft: "Signing in to Ubisoft Connect",
		signingInUbisoftMessage: "Opening Ubisoft Connect — sign in there, then return.",
		installingUbisoft: "Installing via Ubisoft Connect",
		installingUbisoftMessage: "Opening Ubisoft Connect — start the install from the launcher window.",
		ubisoftPrefixNotReady: "Ubisoft Connect isn't ready",
		ubisoftPrefixNotReadyMessage: "Couldn't prepare the Ubisoft Connect prefix for this game. Try installing again; if it keeps failing, re-run a library sync.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect isn't ready",
		ubisoftAuthPrefixNotReadyMessage: "Couldn't prepare Ubisoft Connect for sign-in. Please try again in a moment.",
		ubisoftLaunchIdMissing: "Opening Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "Couldn't resolve this game's launch ID — opening Ubisoft Connect so you can start it from your library. Re-run a library sync to fix direct launch.",
		dependenciesTitle: "Installing Dependencies",
		installingRedistributables: "Installing game redistributables...",
		dependenciesReady: "Dependencies Ready",
		redistributablesInstalled: "Redistributables installed",
		dependenciesStatus: "Dependencies",
		checkLogs: "Setup complete (check logs for details)",
		installingProton: "Installing Proton",
		attemptingInstall: "Attempting to install {{version}}...",
		downloadingProton: "Downloading Proton — this may take a few minutes...",
		protonSwitchedTo: "Now using {{version}}",
		protonReadyTitle: "Proton Ready",
		protonReadyBody: "{{version}} is ready to use",
		epicAuthRequired: "Epic Login Required",
		epicAuthRequiredMessage: "Please sign in to Epic Games in Unifideck settings",
		installingRedist: "Installing Redistributables",
		installingRedistMessage: "Running first-time GOG redistributable installation...",
		setupFailed: "Setup Failed",
		setupFailedMessage: "Installation failed, see logs for details",
		prerequisitesTitle: "Installing Prerequisites",
		installingPrerequisites: "Installing required game prerequisites...",
		prerequisitesReady: "Prerequisites Ready",
		prerequisitesInstalled: "Game prerequisites installed",
		prerequisitesFailed: "Prerequisites Warning",
		prerequisitesFailedMessage: "Prerequisites may not have installed correctly",
		browserRequired: "Microsoft Edge Required",
		signingIn: "Signing In...",
		cloudSyncDownFailed: "Cloud save download failed for {{store}} ({{error}}). Playing with local saves.",
		cloudSyncUpFailed: "Cloud save upload failed for {{store}} ({{error}}). Your progress is stored locally only.",
		errorCircuitBreakerOpen: "{{game_key}} failed to launch {{count}} times recently. Automatic retries paused.",
		launcherError: "Couldn't launch {{game_key}} ({{error_code}}). Check the logs for details."
	},
	updateComplete: "Update Complete!",
	updateCompleteMessage: "{{title}} has been updated.",
	updateCancelled: "Update Cancelled",
	updateCancelledMessage: "Update for {{title}} was cancelled.",
	updateFailed: "Update Failed",
	updateFailedMessage: "Failed to start update.",
	storeError: "Failed to communicate with {{store}} ({{errType}})",
	captureLogsStarted: "Collecting logs...",
	captureLogsStartedMessage: "Building a diagnostics bundle in your Downloads folder. This takes a few seconds.",
	captureLogsDone: "Logs captured",
	captureLogsDoneMessage: "Saved to {{path}} ({{size}}, {{files}} files).",
	captureLogsPartial: "Logs captured (partial)",
	captureLogsPartialMessage: "Saved to {{path}} ({{size}}, {{files}} files). {{failures}} file(s) could not be read.",
	captureLogsFailed: "Could not capture logs",
	captureLogsBusy: "A log capture is already running",
	captureLogsCopied: "Path copied",
	captureLogsCopyFailed: "Could not copy the path",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Free up space",
		signInToStore: "Sign in",
		retrySync: "Retry",
		retryLibrarySync: "Refresh library",
		openSaveFolder: "Show save folder",
		showLogs: "Show logs"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Couldn't refresh {{store}} library ({{error}}). Your existing library stays visible."
	}
};
var uninstallModal$f = {
	title: "Confirm Uninstallation",
	description: "Are you sure you want to uninstall {{title}}?",
	deleteProtonLabel: "Also delete Proton files",
	deleteProtonDescription: "Removes Compatibility and User Files data",
	warningTitle: "Warning:",
	warningBody: "This will permanently delete save data stored in the Wine prefix. Make sure they are backed up.",
	cancel: "Cancel",
	uninstall: "Uninstall"
};
var languageSettings$f = {
	title: "LANGUAGE",
	label: "Display Language",
	description: "Language for UI and game downloads (where supported)",
	autoDetect: "Auto (System)"
};
var errors$f = {
	legendaryNotFound: "Epic Games CLI (Legendary) not found",
	gogdlNotFound: "GOG CLI (gogdl) not found",
	nileNotFound: "Amazon Games CLI (Nile) not found",
	authUrlFailed: "Could not get authentication URL",
	authFailed: "Authentication failed",
	notAuthenticated: "Not authenticated. Please log in first.",
	syncInProgress: "A sync operation is already in progress",
	syncCancelled: "Sync cancelled by user",
	gameNotFound: "Game not found",
	gameNotInstalled: "Game is not installed",
	installFailed: "Installation failed - check logs for details",
	installNoDirectory: "Installation completed but could not locate game directory",
	uninstallFailed: "Uninstallation failed",
	lockConflict: "Lock conflict - please try again",
	unknownStore: "Unknown store",
	shortcutWriteFailed: "Failed to write shortcuts",
	backgroundSyncDisabled: "Background sync disabled",
	steamGridDbUnavailable: "SteamGridDB library not available",
	deleteSyncInProgress: "Cannot delete while sync is in progress",
	alreadyInQueue: "Already in queue",
	download: {
		diskSpace: "Not enough disk space available",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Network error — check your connection",
		authExpired: "Login expired — please sign in again",
		toolMissing: "Required download tool not found",
		processFailed: "Download failed — please try again",
		directoryNotFound: "Could not locate installed files",
		lockConflict: "Another operation is in progress — try again",
		ubisoftClient: "Ubisoft Connect client error — try repairing the prefix",
		ubisoftClientNotRunning: "Ubisoft Connect client not responding",
		ubisoftBootstrap: "Failed to set up Ubisoft Connect prefix",
		ubisoftUmuMissing: "umu-run not found — required for Ubisoft games",
		generic: "Installation failed — check logs for details"
	},
	unknown: "Unknown error",
	steamAppsUnavailable: "SteamClient.Apps API not available"
};
var sync$f = {
	start: "Sync Libraries",
	force: "Force Sync",
	syncing: "Syncing…",
	cancel: "Cancel Sync",
	cancelling: "Cancelling…",
	preparing: "Preparing…",
	downloadingArtwork: "Downloading artwork for {{game}}...",
	fetchingGameLists: "Fetching game lists...",
	fetchingStore: "Fetching {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Sync cancelled by user",
	checkingInstalledGames: "Checking installed games...",
	sgdbLookup: "Looking up {{count}} games on SteamGridDB...",
	extractingSteamMetadata: "Extracting Steam metadata...",
	fetchingEnhancedMetadata: "Fetching enhanced metadata...",
	lookingUpUnifiDB: "Fetching unifiDB metadata...",
	fetchingMetacriticData: "Fetching Metacritic metadata...",
	fetchingCompatData: "Checking compatibility data...",
	checkingExistingArtwork: "Checking existing artwork...",
	cleaningOrphanedArtwork: "Cleaning up orphaned artwork...",
	retryingMissingArtwork: "Retrying {{synced}} / {{total}} games with missing artwork...",
	savingShortcuts: "Saving shortcuts...",
	completed: "Sync completed!",
	extractingMetadata: "Extracting metadata for {{game}}...",
	cooldown: "Wait {{secs}}s",
	artworkPhase: "Artwork {{done}} / {{total}}"
};
var force_sync$f = {
	migratingOldInstallations: "Force sync: Migrating old installations...",
	fetchingGameLists: "Force sync: Fetching game lists...",
	checkingInstalledGames: "Force sync: Checking installed games...",
	rewritingShortcuts: "Force sync: Rewriting shortcuts...",
	cancelledShortcutsSaved: "Force sync cancelled - shortcuts saved",
	writingShortcuts: "Force Sync: Writing shortcuts...",
	clearingProton: "Force sync: Clearing Proton compatibility (launcher manages internally)...",
	completed: "Force sync completed! Updated {{updated}} games, fetched {{artwork}} artwork."
};
var artwork$f = {
	checking: "Checking artwork...",
	queueRefresh: "Queuing all games for artwork refresh...",
	downloadProgress: "Downloaded artwork {{synced}}/{{total}} ({{game}})"
};
var deckTabs$f = {
	greatOnDeck: "Great on Deck",
	allGames: "All Games",
	installed: "Installed",
	nonSteam: "Non-Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$f = {
	error: "Unifideck Error",
	errorLoadingView: "Failed to load unified library view",
	checkConsole: "Check browser console for details",
	errorLoadingGames: "Error loading games",
	tryReloading: "Try reloading the plugin or checking the console for details",
	storeLabel: "Store:",
	allStores: "All Stores",
	searchPlaceholder: "Search games...",
	library: "Library"
};
var gameInfoPanel$f = {
	loading: "Loading game information...",
	noMetadata: "No metadata available for this game.",
	noTestResults: "No detailed test results available.",
	buttons: {
		details: "Details",
		synopsis: "Synopsis",
		achievements: "Achievements",
		storePage: "Store Page",
		dlc: "DLC",
		communityHub: "Community Hub",
		pointsShop: "Points Shop",
		discussions: "Discussions",
		guides: "Guides",
		support: "Support",
		install: "Install",
		uninstall: "Uninstall",
		cancel: "Cancel"
	},
	labels: {
		store: "Store:",
		name: "Name:",
		size: "Size:",
		developer: "Developer:",
		publisher: "Publisher:",
		released: "Released:",
		metacritic: "Metacritic:",
		status: "Status:",
		spaceRequired: "Space Required",
		lastPlayed: "Last Played",
		playTime: "Play Time",
		never: "Never"
	},
	compatibility: {
		modalTitle: "Steam Deck Compatibility",
		viewOnProtonDb: "View on ProtonDB",
		close: "Close",
		verified: "VERIFIED",
		playable: "PLAYABLE",
		unsupported: "UNSUPPORTED",
		unknown: "UNKNOWN"
	},
	failedToLoadMetadata: "Failed to load metadata",
	lastSession: "Last session: unlocked {{names}}"
};
var achievements$f = {
	unlockedTitle: "Achievement Unlocked",
	unlockedBody: "{{name}}"
};
var achievementsModal$f = {
	title: "Achievements",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Refresh",
	close: "Close",
	loading: "Loading achievements…",
	empty: "This game has no achievements.",
	hidden: "Hidden achievement",
	unlockedOn: "Unlocked {{date}}",
	error: "Couldn't load achievements.",
	errorOffline: "Connect to the internet to view achievements.",
	errorAuth: "Sign in to this game's store to view achievements.",
	errorUnavailable: "Achievements aren't available for this game."
};
var gameDetailsSettings$f = {
	title: "GAME DETAILS VIEW",
	simple: "Simple",
	detailed: "Detailed"
};
var collectionSettings$f = {
	title: "STEAM COLLECTIONS",
	enabled: "Collections enabled",
	disabled: "Collections disabled"
};
var gogLanguageModal$f = {
	title: "Select Language",
	description: "Choose which language to download for {{title}}",
	label: "Language",
	install: "Install",
	cancel: "Cancel"
};
var accountSwitch$f = {
	title: "Account Switch Detected",
	description: "A different Steam account is now logged in. What would you like to do with your Unifideck data?",
	migrate: "Migrate Unifideck Data",
	migrateDescription: "Copy games & artwork from previous account",
	freshStart: "Fresh Start",
	freshStartDescription: "Clear store logins to connect different Epic/GOG/Amazon/Ubisoft accounts",
	skip: "Skip",
	toastMigrateTitle: "Migration Complete",
	toastMigrateBody: "Migrated {{shortcuts}} shortcuts and {{artwork}} artwork files",
	toastClearTitle: "Auth Tokens Cleared",
	toastClearBody: "Store logins have been cleared. You can now sign in with different accounts."
};
var ubisoftAuth$f = {
	title: "Ubisoft Connect Sign In",
	description: "Sign in through Ubisoft Connect to link your library.",
	signInViaUpc: "Sign In via Ubisoft Connect",
	upcDescription: "Ubisoft Connect will open for you to sign in. Once you log in, your session will be captured automatically.",
	launchingUpc: "Launching Ubisoft Connect...",
	waitingForLogin: "Waiting for login...",
	retryLaunch: "Retry",
	cancel: "Cancel",
	launchFailed: "Failed to launch Ubisoft Connect. Please try again.",
	signingIn: "Signing In...",
	emailLabel: "Email",
	passwordLabel: "Password",
	signIn: "Sign In",
	twoFactorTitle: "Two-Factor Authentication",
	twoFactorDescription: "Enter the verification code from your authenticator app.",
	twoFactorLabel: "Verification Code",
	verify: "Verify",
	verifying: "Verifying...",
	errorEmptyFields: "Please enter your email and password",
	errorEmpty2FA: "Please enter the verification code",
	errorLoginFailed: "Login failed. Please check your credentials.",
	errorInvalid2FA: "Invalid verification code. Please try again.",
	showPassword: "Show password",
	hidePassword: "Hide password",
	doubleLoginWarning: "You will need to sign in twice for Ubisoft."
};
var relativeDate$f = {
	today: "Today",
	yesterday: "Yesterday"
};
var common$f = {
	thisGame: "this game",
	game: "Game",
	unknown: "Unknown",
	ok: "OK",
	cancel: "Cancel",
	close: "Close",
	working: "Working…",
	loading: "Loading…",
	free: "free"
};
var info$f = {
	expandDetails: "Show details",
	collapseDetails: "Hide details"
};
var microsoft$f = {
	notCompatible: "This game format is not compatible with SteamOS",
	playOnCloud: "Play on Cloud",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Sign in with your Microsoft / Xbox account",
	accountConnected: "Microsoft account connected",
	loggedOut: "Logged out from Microsoft Store",
	xcloudNotInstalled: "xCloud games are not installed locally",
	chromiumRequired: "Microsoft Edge Required",
	chromiumRequiredMessage: "Login requires Microsoft Edge. Would you like to install it automatically? (This only needs to be done once.)",
	chromiumInstallButton: "Install Microsoft Edge",
	chromiumInstalling: "Installing Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge installed!",
	chromiumAlreadyInstalled: "A compatible browser is already installed",
	chromiumInstallFailed: "Microsoft Edge installation failed. Please install manually: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Microsoft Edge installation timed out. Please check your internet connection.",
	browserAlreadyInstalled: "A compatible browser is already installed",
	browserInstalled: "Microsoft Edge installed successfully!",
	browserInstallFailed: "Microsoft Edge installation failed. Please install manually: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak is not available on this system",
	signInRequired: "Sign in to play",
	syncSkippedNoSubscription: "No active Xbox Game Pass subscription detected. Your xCloud library was not synchronized.",
	syncSkippedTierUnknown: "Your Xbox Game Pass subscription was detected but the plan could not be determined. Sync skipped to avoid listing unavailable games.",
	subscriptionCheckFailed: "Could not verify your Xbox Game Pass subscription. xCloud sync skipped. Retry when your connection stabilizes.",
	subscriptionDetected: "Xbox Game Pass {{tier}} subscription active."
};
var playButton$f = {
	play: "Play",
	install: "Install",
	cancel: "Cancel",
	retry: "Retry",
	downloadFailed: "Download failed",
	installing: "Installing",
	controllerConfig: "Controller configuration",
	appSettings: "App settings"
};
var play$f = {
	play: "Play",
	resume: "Resume",
	stop: "Stop",
	cancel: "Cancel",
	cancelling: "Cancelling…",
	update: "Update",
	uninstall: "Uninstall",
	queued: "Queued",
	downloading: "Downloading",
	extracting: "Extracting",
	verifying: "Verifying",
	finalizing: "Finalising install",
	cancelConfirmTitle: "Cancel download?",
	cancelConfirmBody: "Are you sure you want to cancel the download for {{title}}? Partial files will be discarded.",
	cancelConfirmConfirm: "Cancel download",
	cancelConfirmCancel: "Keep downloading",
	playOnCloud: "Play on Cloud",
	cloudSave: {
		label: "Cloud saves",
		menuTitle: "Cloud saves — {{game}}",
		close: "Close",
		download: "Download cloud save",
		upload: "Upload local save",
		statusSyncing: "Syncing cloud saves…",
		statusCloudAvailable: "Cloud save available",
		statusNoSaves: "No local saves",
		filesCount: "{{count}} files",
		statusUnresolved: "Save location not found — sync may not work for this game.",
		unresolvedHint: "Launch the game once so we can detect its save folder, or set the location manually.",
		setLocation: "Set save location",
		setLocationTitle: "Choose this game's save folder",
		setLocationHint: "Pick the folder where this game stores its saves.",
		noCloudSupport: "No cloud-save support",
		noCloudData: "Unknown",
		autoPullOn: "Cloud saves download automatically on launch."
	},
	exe: {
		menuItem: "Change executable…",
		title: "Change executable — {{game}}",
		subtitle: "Pick which file this game launches (e.g. skip a launcher and run the game directly). The working directory and cloud saves are unaffected.",
		defaultTag: "(default)",
		browse: "Browse…",
		resetButton: "Reset to default",
		unavailable: "Couldn't read this game's folder.",
		changed: "Executable changed",
		changeFailed: "Couldn't change executable",
		reset: "Reset to default executable"
	}
};
var playMeta$f = {
	spaceRequired: "Space required",
	installedSize: "Installed size",
	lastPlayed: "Last played",
	neverPlayed: "Never played",
	played: "Played",
	hoursShort: "h",
	minutesShort: "min"
};
var downloads$f = {
	title: "Downloads",
	empty: "No downloads. Install games from your library to see them here.",
	current: "Now downloading",
	queued: "Queued",
	finished: "Recently finished",
	cancel: "Cancel",
	removeFromQueue: "Remove",
	play: "Play",
	outcome: {
		queued: "Queued",
		downloading: "Downloading",
		completed: "Complete",
		cancelled: "Cancelled",
		error: "Failed"
	}
};
var navigation$f = {
	loading: "Loading...",
	storesTitle: "Stores",
	syncTitle: "Library Sync",
	languageTitle: "Language"
};
var security$f = {
	panelTitle: "Security",
	counters: {
		label: "Counters",
		empty: "No events observed since startup"
	},
	log: {
		label: "Audit log",
		entryCount: "{{count}} entry",
		entryCount_other: "{{count}} entries",
		emptyFiltered: "No events matching the filter"
	},
	filter: {
		label: "Filter",
		all: "All events"
	},
	timeline: {
		label: "24h timeline ({{count}} event)",
		label_other: "24h timeline ({{count}} events)",
		axisStart: "-24h",
		axisEnd: "now"
	},
	relativeTime: {
		seconds: "{{count}}s ago",
		minutes: "{{count}} min ago",
		hours: "{{count}}h ago",
		days: "{{count}}d ago"
	},
	bruteforce: {
		warningTitle: "Warning: brute-force detection (warning level)",
		escalationTitle: "ALERT: brute-force detection (escalation level)",
		detail: "{{failures}} decryption failures in the last {{window}}s. Thresholds: {{warning}} (warning) / {{escalation}} (escalation).",
		resetButton: "Acknowledge and reset the detector"
	},
	actions: {
		clearLog: "Clear log and counters"
	},
	events: {
		tokenEncrypted: "Token encrypted",
		tokenDecrypted: "Token decrypted",
		decryptFailed: "Decryption failed",
		tokenFileMigrated: "Token file migrated",
		legacyPlaintextDetected: "Legacy plaintext detected",
		authFlowStarted: "Auth started",
		authFlowCompleted: "Auth completed",
		authFlowFailed: "Auth failed",
		permissionsCheck: "Permissions checked",
		permissionsRepaired: "Permissions repaired",
		bruteforceSuspected: "Brute-force suspected",
		deviceResetDetected: "Device reset detected",
		fingerprintInitialized: "Fingerprint initialized",
		externalAuthCheckFailed: "External auth check failed",
		configValidationCompleted: "Config validation completed",
		configValidationFailed: "Config validation failed"
	},
	configValidation: {
		degradedTitle: "Configuration invalid - degraded mode active",
		detail: "{{count}} validation error detected at boot",
		detail_other: "{{count}} validation errors detected at boot",
		sourceDefaults: "defaults",
		sourceUser: "user overrides",
		sourceMerged: "merged",
		overflow: "+ {{count}} more error",
		overflow_other: "+ {{count}} more errors"
	}
};
var cloudSyncBehavior$f = {
	_comment: "Settings panel letting the user pick, per store, how cloud save sync failures should be surfaced. The five store_label entries mirror the rows rendered by CloudSyncBehaviorPanel.tsx. Microsoft deliberately omitted — xCloud streaming has no local save to sync.",
	title: "Cloud save sync behavior",
	description: "Choose how Unifideck should react when a cloud save download or upload fails at game launch.",
	loadError: "Could not load current settings",
	saveError: "Could not save your change — please retry",
	store_label: {
		"default": "Default (all stores)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Silent (log only)",
		toast: "Notify (toast)"
	}
};
var cloudSync$f = {
	_comment: "Toast strings emitted by the launcher when a cloud sync fails. The error placeholder is resolved through cloudSync.error.<code> via the backend payload's error_i18n_key field.",
	unavailableNativeDep: "Cloud saves are off on this system — its Python ({{python}}) is missing a required component. Your local saves are unaffected; rebuild/reinstall the plugin after a system update to restore sync.",
	error: {
		network_unreachable: "Network unreachable",
		auth_expired: "Authentication expired",
		forbidden: "Access denied",
		quota_exceeded: "Cloud quota exceeded",
		server_error: "Store server unavailable",
		timed_out: "Request timed out",
		disk_full: "Not enough disk space",
		disk_space_low: "Skipped — low disk space (free up some room and try again)",
		permission_denied: "Permission denied",
		cancelled: "Cancelled",
		unknown: "Unexpected error"
	}
};
var launchLogs$f = {
	_comment: "LaunchLogsModal strings. The modal opens from the unifideck://show-logs/<launch_id> toast action, attached to launcher-failure toasts (circuit breaker + LauncherError). Reads per-launch archived logs from ~/.local/share/unifideck/launches/<launch_id>.log and renders them with level-based color coding.",
	title: "Launch logs — {{launchId}}",
	close: "Close",
	copyAll: "Copy all",
	"export": "Export to file",
	copied: "Copied to clipboard",
	copyFailed: "Copy failed",
	exporting: "Exporting…",
	exportedTo: "Exported to {{path}}",
	exportFailed: "Export failed: {{error}}",
	loading: "Loading logs…",
	notAvailable: "Logs for this launch are no longer available (past the 7-day retention window).",
	error: "Couldn't read the logs: {{error}}",
	fileInfo: "{{path}} — showing {{shown}} of {{total}} lines",
	filterLabel: "Show",
	filterInfo: "Info",
	filterWarning: "Warning",
	filterError: "Error",
	nextError: "Next error ({{current}}/{{total}})",
	noErrors: "No errors",
	allFiltered: "All lines hidden by the current filter. Enable at least one level to see log entries."
};
var settings$f = {
	_comment: "UnifideckSettingsPanel action-type controls that aren't covered by more specific components (LanguageSelector, CloudSyncBehaviorPanel). refreshAll is a fire-and-forget button that triggers sync_all() across every registered store; the Library view shows the sync progress via its own subscription to SYNC_PROGRESS events.",
	storeConnections: "Store Connections",
	librarySync: "Library Sync",
	language: "Language",
	languageAuto: "Auto-detect",
	refreshAll: {
		sectionTitle: "Library actions",
		button: "Refresh all libraries",
		buttonBusy: "Starting…",
		startedTitle: "Library refresh started",
		startedBody: "Syncing all stores in the background.",
		startedSingle: "Refreshing {{store}} library in the background.",
		failedTitle: "Couldn't start library refresh"
	}
};
var saveFolder$f = {
	_comment: "SaveFolderModal strings. The modal opens from the unifideck://open-save-folder/<store>/<game_id> toast action, typically surfaced on cloud sync failures where the user wants to inspect what's actually stored locally. count and size are interpolated; truncated shows only when the result was capped.",
	title: "Save folder — {{store}} / {{gameId}}",
	close: "Close",
	copyPath: "Copy path",
	copyFileList: "Copy file list",
	depthLabel: "Depth",
	filterLabel: "Filter",
	loading: "Loading folder contents…",
	noSavesYet: "No saves found locally for this game yet. The folder will be created on the next successful cloud sync.",
	aggregate: "{{count}} files, {{size}} total",
	truncated: "{{count}} more files ({{size}}) not shown — narrow your filter to see them.",
	error: "Couldn't read the save folder: {{error}}"
};
var library$f = {
	_comment: "Per-game UI affordances rendered in the Library view, including the circuit-breaker badge and force-launch / reset controls when launches fail repeatedly.",
	circuitBreaker: {
		_comment: "Surface for the launch-failure circuit breaker. Badge appears only when failures within the configured window reach circuit_breaker.failures_threshold (default 3 in 600s). Force launch arms a one-shot bypass; Reset wipes the failure history entirely.",
		badgeRecent: "{{count}} recent launch failure",
		badgeRecent_other: "{{count}} recent launch failures",
		badgeTooltip: "{{count}} failed launches in the recent time window — the circuit breaker has paused automatic retries",
		forceLaunchButton: "Force launch",
		resetButton: "Reset failures",
		failureKind: {
			fastBoot: "Fast boot crash",
			launcherError: "Launch setup error"
		}
	}
};
var pickStorage$f = {
	title: "Install Location",
	customOption: "Choose Another Location...",
	customLabel: "Custom Location",
	change: "Change...",
	gameSize: "Install Size"
};
var restart$f = {
	title: "Sync Complete!",
	body: "Please Restart Steam to apply your new games and artwork",
	confirm: "Restart Now",
	later: "Later"
};
var updater$f = {
	checkButton: "Check for Updates",
	checkCompleteMessage: "Successfully fetched latest version info.",
	checkCompleteTitle: "Update Check Complete",
	checkFailedTitle: "Check Failed",
	checkingButton: "Checking...",
	currentTitle: "Current",
	downgradeButton: "Downgrade to v{{version}}",
	installFailedTitle: "Install Failed",
	installTimeoutMessage: "No response from Decky Loader — the install may have stalled or failed. Please try again.",
	installedLabel: "installed",
	installingButton: "Installing...",
	installingTitle: "Installing Plugin",
	latestLabel: "latest",
	modalTitle: "UNIFIDECK v{{version}} — Release Notes",
	noBackend: "Decky backend is unavailable.",
	noReleaseNotes: "No release notes available.",
	reinstallButton: "Reinstall v{{version}}",
	refreshFailedMessage: "Could not verify the latest release info. Check your connection and try again.",
	releaseGoneMessage: "This release is no longer available. Please Check for Updates and select again.",
	releaseNotesButton: "Release Notes",
	stageDownload: "Downloading…",
	stageFinish: "Finishing…",
	stageInstall: "Installing…",
	stageOpen: "Reading package…",
	stageParse: "Verifying…",
	stageRemove: "Removing old version…",
	stageStart: "Starting…",
	stageWorking: "Installing…",
	titleLoading: "Checking version",
	typeDowngrade: "Downgrading to",
	typeReinstall: "Reinstalling",
	typeUpdate: "Updating to",
	updateButton: "Update to v{{version}}"
};
var enUS = {
	_comment: _comment$f,
	tabs: tabs$f,
	auth: auth$f,
	storage: storage$f,
	storageSettings: storageSettings$f,
	gameGrid: gameGrid$f,
	downloadsTab: downloadsTab$f,
	cloudSave: cloudSave$f,
	storeConnections: storeConnections$f,
	librarySync: librarySync$f,
	cleanup: cleanup$f,
	captureLogs: captureLogs$f,
	installButton: installButton$f,
	confirmModals: confirmModals$f,
	toasts: toasts$f,
	uninstallModal: uninstallModal$f,
	languageSettings: languageSettings$f,
	errors: errors$f,
	sync: sync$f,
	force_sync: force_sync$f,
	artwork: artwork$f,
	deckTabs: deckTabs$f,
	unifiedLibrary: unifiedLibrary$f,
	gameInfoPanel: gameInfoPanel$f,
	achievements: achievements$f,
	achievementsModal: achievementsModal$f,
	gameDetailsSettings: gameDetailsSettings$f,
	collectionSettings: collectionSettings$f,
	gogLanguageModal: gogLanguageModal$f,
	accountSwitch: accountSwitch$f,
	ubisoftAuth: ubisoftAuth$f,
	relativeDate: relativeDate$f,
	common: common$f,
	info: info$f,
	microsoft: microsoft$f,
	playButton: playButton$f,
	play: play$f,
	playMeta: playMeta$f,
	downloads: downloads$f,
	navigation: navigation$f,
	security: security$f,
	cloudSyncBehavior: cloudSyncBehavior$f,
	cloudSync: cloudSync$f,
	launchLogs: launchLogs$f,
	settings: settings$f,
	saveFolder: saveFolder$f,
	library: library$f,
	pickStorage: pickStorage$f,
	restart: restart$f,
	updater: updater$f
};

var _comment$e = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$e = {
	settings: "Paramètres",
	downloads: "Téléchargements"
};
var auth$e = {
	connect: "Se connecter",
	disconnect: "Se déconnecter",
	status: {
		connected: "Connecté",
		disconnected: "Non connecté",
		checking: "Vérification…",
		error: "Erreur — vérifier les journaux"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$e = {
	title: "Stockage",
	defaultUpdated: "Stockage par défaut mis à jour",
	isDefault: "Par défaut",
	setDefault: "Définir par défaut",
	customPath: "Chemin personnalisé",
	up: "↑ Parent",
	refresh: "Actualiser",
	useThisPath: "Utiliser ce chemin"
};
var storageSettings$e = {
	title: "Paramètres de téléchargement",
	installLocationLabel: "Emplacement d'installation",
	installLocationDescription: "Emplacement où les nouveaux jeux seront téléchargés",
	pathLabel: "Chemin",
	loading: "Chargement des options de stockage...",
	toastUpdatedTitle: "Emplacement de stockage mis à jour",
	toastUpdatedBody: "Les nouveaux jeux seront installés dans {{location}}",
	toastFailedTitle: "Échec de la mise à jour",
	toastFailedBody: "{{error}}",
	unknown: "Inconnu",
	browseButton: "Parcourir...",
	clearCustom: "Effacer l'emplacement personnalisé",
	customPathSet: "Emplacement d'installation personnalisé défini",
	newFolder: "Nouveau dossier",
	newFolderCreate: "Créer",
	selectFolder: "Utiliser ce dossier",
	emptyDirectory: "Dossier vide",
	showHidden: "Afficher les fichiers cachés",
	selectDevice: "Sélectionner le périphérique",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Modifié (le plus récent)",
	sortModifiedOldest: "Modifié (le plus ancien)",
	sortCreatedNewest: "Créé (le plus récent)",
	sortCreatedOldest: "Créé (le plus ancien)",
	sortSizeLargest: "Taille (la plus grande)",
	sortSizeSmallest: "Taille (la plus petite)",
	"validationErrors.notAbsolute": "Le chemin doit être absolu",
	"validationErrors.systemDir": "Impossible d'utiliser un dossier système",
	"validationErrors.notWritable": "L'emplacement n'est pas accessible en écriture",
	"validationErrors.notFound": "Le dossier n'existe pas",
	"validationErrors.homeRoot": "Impossible d'utiliser la racine du dossier personnel",
	freeSpaceLabel: "{{label}} ({{freeSpace}} Go libres)"
};
var gameGrid$e = {
	loading: "Chargement des jeux...",
	noGamesFound: "Aucun jeu trouvé",
	noGamesHint: "Essayez de synchroniser vos bibliothèques depuis les paramètres Unifideck",
	showingGames: "Affichage de {{count}} jeux",
	installed: "Installé",
	notInstalled: "Non installé",
	playtime: "{{hours}} h {{minutes}} m jouées"
};
var downloadsTab$e = {
	currentDownload: "TÉLÉCHARGEMENT EN COURS",
	queuedDownloads: "EN FILE D'ATTENTE ({{count}})",
	recentlyCompleted: "RÉCEMMENT TERMINÉ",
	noActiveDownloads: "Aucun téléchargement en cours",
	noDownloads: "Aucun téléchargement. Installez des jeux depuis votre bibliothèque pour les voir ici.",
	loadingLabel: "Chargement...",
	loadingMessage: "Récupération de la file d'attente...",
	confirmCancelTitle: "Confirmer l'annulation",
	confirmCancelDescription: "Êtes-vous sûr de vouloir annuler le téléchargement de {{game}} ?",
	confirmCancelYes: "Oui",
	confirmCancelNo: "Non",
	cancelDownload: "Annuler",
	toastDownloadCancelledTitle: "Téléchargement annulé",
	toastDownloadCancelledBody: "Le téléchargement a été retiré de la file d'attente.",
	toastCancelFailedTitle: "Échec de l'annulation",
	toastCancelFailedBody: "Impossible d'annuler le téléchargement : {{error}}",
	status: {
		queued: "En file d'attente",
		downloading: "Téléchargement en cours",
		completed: "Terminé",
		cancelled: "Annulé",
		error: "Erreur"
	},
	preparingDownload: "Préparation du téléchargement...",
	phaseExtracting: "Extraction de {{game}}...",
	phaseVerifying: "Vérification de {{game}}...",
	internalStorage: "Stockage interne ({{freeSpace}} Go libres)",
	sdCard: "Carte SD ({{freeSpace}} Go libres)",
	customLocation: "Personnalisé : {{path}} ({{freeSpace}} Go libres)",
	extractingLabel: "Extraction...",
	verifyingLabel: "Vérification...",
	updateQueuedLabel: "Mise à jour en file d'attente",
	downloadQueuedLabel: "Téléchargement en file d'attente",
	downloadingUpdateLabel: "Téléchargement de la mise à jour",
	downloadingLabel: "Téléchargement en cours...",
	finalizingInstallation: "Finalisation de l'installation...",
	speedMbps: "{{speed}} Mo/s",
	etaLabel: "Temps restant : {{eta}}",
	preparingMessage: "Initialisation du préfixe et synchronisation des sauvegardes cloud...",
	preparingLabel: "Configuration du jeu...",
	installingViaUpcLabel: "Installation via Ubisoft Connect",
	extractingMessage: "Extraction des fichiers...",
	verifyingMessage: "Vérification des fichiers... {{pct}}%",
	installingViaUpcMessage: "Suivez la fenêtre Ubisoft Connect pour terminer l'installation..."
};
var cloudSave$e = {
	title: "Conflit de sauvegardes cloud",
	description: "Vos sauvegardes locales diffèrent des sauvegardes cloud pour {{game}}",
	local: "Locale :",
	cloud: "Cloud :",
	newer: "(plus récente)",
	useCloud: "Utiliser le cloud",
	useLocal: "Utiliser le stockage local",
	unknownDate: "Inconnu",
	toastUsingCloudTitle: "Utilisation des sauvegardes cloud",
	toastUsingCloudBody: "Téléchargement des sauvegardes cloud pour {{game}}",
	toastUsingLocalTitle: "Utilisation des sauvegardes locales",
	toastUsingLocalBody: "Téléchargement des sauvegardes locales pour {{game}}",
	toastErrorTitle: "Erreur",
	toastErrorBody: "Échec de la résolution du conflit",
	filesSize: "{{count}} fichiers · {{size}}",
	uploadBlockedEmpty: "Envoi vers le cloud ignoré pour {{game}} — aucune sauvegarde locale trouvée",
	uploadSkippedTitle: "Envoi vers le cloud ignoré",
	uploadSkippedBody: "Aucune sauvegarde locale à envoyer pour l'instant pour {{game}}."
};
var storeConnections$e = {
	title: "CONNEXIONS AUX BOUTIQUES",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Connecté",
	checking: "Vérification...",
	authenticate: "Se connecter",
	legendaryNotInstalled: "⚠️ Installation de l'outil Epic Games...",
	nileNotInstalled: "⚠️ Outil Amazon Games introuvable",
	error: "Erreur — vérifier les journaux"
};
var librarySync$e = {
	title: "SYNCHRO DE LA BIBLIOTHÈQUE",
	syncLibraries: "Synchroniser les bibliothèques",
	forceSync: "Forcer la synchronisation",
	syncing: "Synchronisation...",
	cancelSync: "Annuler la synchronisation",
	retryStatusCheck: "Réessayer la vérification du statut",
	gamesSynced: "{{synced}} / {{total}} jeux synchronisés",
	artworkDownloaded: "{{synced}} / {{total}} images de couverture téléchargées",
	metadataExtracted: "{{synced}} / {{total}} fiches de métadonnées extraites",
	steamMetadataDownloaded: "Métadonnées Steam : {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "Métadonnées unifiDB : {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Données Metacritic : {{synced}} / {{total}}",
	rawgMetadataDownloaded: "Métadonnées RAWG : {{synced}} / {{total}}",
	compatChecked: "Données de compatibilité : {{synced}} / {{total}}",
	cancelling: "Annulation…",
	error: "Erreur"
};
var cleanup$e = {
	title: "Nettoyage",
	deleteAll: "Supprimer toutes les bibliothèques et le cache UNIFIDECK",
	modalTitle: "Supprimer toutes les données Unifideck ?",
	modalDescription: "Cela supprimera tous les raccourcis Unifideck, les images de couverture Steam, les connexions sauvegardées et les données mises en cache. Les jeux installés restent sur le disque et peuvent être resynchronisés. Activez le mode destructif ci-dessous pour supprimer également les fichiers de jeu installés.",
	deleteFilesLabel: "Supprimer aussi les fichiers de jeu installés (Destructif)",
	deleteFilesDescription: "Supprime le répertoire d'installation sur le disque pour chaque jeu Unifideck sous une racine sécurisée.",
	confirmDelete: "Oui, tout supprimer",
	cancel: "Annuler",
	deleting: "Suppression en cours..."
};
var captureLogs$e = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (fr-FR).",
	title: "Capture des journaux",
	description: "Rassemble tous les journaux Unifideck dans une seule archive zip dans votre dossier Téléchargements, pour la joindre à un rapport de bug.",
	capture: "Capturer les journaux",
	capturing: "Collecte des journaux...",
	savedTo: "Enregistré dans votre dossier Téléchargements",
	bundleInfo: "{{files}} fichiers · {{size}}",
	partialNote: "{{count}} fichier n'a pas pu être lu. L'archive reste utilisable.",
	partialNote_other: "{{count}} fichiers n'ont pas pu être lus. L'archive reste utilisable.",
	copyPath: "Copier le chemin",
	modalTitle: "Créer une archive de diagnostic ?",
	modalDescription: "Unifideck va rassembler ses journaux dans un fichier zip unique dans votre dossier Téléchargements, pour que vous puissiez le joindre à un rapport de bug.",
	modalIncludes: "Inclus : journaux du plugin et des lancements de jeux, historique de synchronisation et d'installation, les titres de vos jeux et leurs chemins d'installation, ainsi que des informations sur votre appareil (modèle, système d'exploitation, stockage).",
	modalExcludes: "Jamais inclus : vos mots de passe, jetons de connexion aux boutiques ou cookies de navigateur.",
	modalConfirm: "Créer l'archive",
	cancel: "Annuler"
};
var installButton$e = {
	play: "Jouer",
	resume: "Reprendre",
	install: "Installer {{title}}",
	installNative: "Installer",
	uninstall: "Désinstaller {{title}}",
	cancel: "✖ Annuler",
	processing: "Traitement en cours...",
	checking: "Vérification...",
	size: "{{size}} Go",
	update: "MISE À JOUR",
	updateAvailable: "MISE À JOUR DISPONIBLE",
	updateAvailableDescription: "Une nouvelle version est prête à être installée."
};
var confirmModals$e = {
	installTitle: "Confirmer l'installation",
	installDescription: "Êtes-vous sûr de vouloir installer {{title}} ?",
	uninstallTitle: "Confirmer la désinstallation",
	uninstallDescription: "Êtes-vous sûr de vouloir désinstaller {{title}} ?",
	cancelTitle: "Confirmer l'annulation",
	cancelDescription: "Êtes-vous sûr de vouloir annuler le téléchargement de {{title}} ?",
	forceSyncTitle: "Forcer la synchronisation",
	forceSyncDescription: "Voulez-vous resynchroniser toutes les images de couverture ? Cela écrasera les modifications manuelles que vous avez apportées aux images. Sélectionnez 'Conserver les images' pour ne télécharger que les images manquantes.",
	resyncArtwork: "Resynchroniser les images",
	keepArtwork: "Conserver les images",
	steamRestartTitle: "Synchronisation terminée !",
	steamRestartDescription: "Veuillez redémarrer Steam pour appliquer vos nouveaux jeux et images de couverture",
	steamRestartDescriptionStore: "Veuillez redémarrer Steam pour appliquer vos nouveaux jeux et images {{store}}",
	steamRestartCleanupTitle: "Nettoyage terminé !",
	steamRestartCleanupDescription: "Veuillez redémarrer Steam pour supprimer complètement les jeux et images de couverture supprimés",
	restartNow: "Redémarrer maintenant",
	later: "Plus tard",
	ubisoftInstallTitle: "Ubisoft Connect requis",
	ubisoftInstallDescription: "Cela chargera le lanceur Ubisoft Connect dont vous aurez besoin pour installer {{title}}.",
	ubisoftInstallConfirm: "Ouvrir Ubisoft Connect",
	yes: "Oui",
	no: "Non",
	amazonUpdateTitle: "Re-téléchargement complet requis",
	amazonUpdateDescription: "Les mises à jour Amazon nécessitent un re-téléchargement complet de {{title}}. Cela peut prendre un certain temps. Continuer ?",
	updateConfirm: "Mettre à jour"
};
var toasts$e = {
	downloadStarted: "Téléchargement démarré. Vérifiez UNIFIDECK > Téléchargements",
	downloadQueued: "Téléchargement en file d'attente",
	downloadQueuedBody: "{{title}} a été ajouté à la file d'attente.",
	downloadFailedUnknown: "Erreur inconnue.",
	updateQueued: "Mise à jour en file d'attente",
	uninstallDone: "Désinstallation terminée.",
	ubisoftLauncherOpening: "Ouverture d'Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect s'ouvre pour {{title}}. Installez le jeu là-bas, puis revenez pour jouer.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect est déjà ouvert pour cette installation.",
	ubisoftLauncherOpenFailed: "Impossible d'ouvrir Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Échec de l'ouverture d'Ubisoft Connect pour l'installation.",
	multipartDetected: "Téléchargement multi-parties détecté",
	multipartMessage: "Veuillez patienter pendant la finalisation",
	downloadFailed: "Échec du téléchargement",
	downloadFailedMessage: "Impossible de démarrer le téléchargement.",
	downloadCancelled: "Téléchargement annulé",
	downloadCancelledMessage: "Téléchargement de {{title}} annulé.",
	cancelFailed: "Échec de l'annulation",
	cancelFailedMessage: "Impossible d'annuler le téléchargement.",
	uninstalling: "Désinstallation...",
	uninstallingMessage: "Désinstallation de {{title}}...",
	uninstallingMessageProton: "Désinstallation de {{title}} et des fichiers Proton...",
	uninstallComplete: "Désinstallation terminée",
	uninstallCompleteMessage: "{{title}} a été supprimé.",
	uninstallCompleteMessageProton: "{{title}} a été supprimé (y compris les fichiers Proton).",
	installComplete: "Installation terminée !",
	installCompleteMessage: "{{title}} est prêt à jouer.",
	syncComplete: "Synchronisation terminée !",
	syncCompleteMessage: "{{count}} jeux ajoutés. REDÉMARREZ STEAM (fermez-le complètement, ne vous contentez pas de revenir en mode jeu) pour les voir dans votre bibliothèque.",
	forceSyncComplete: "Synchronisation forcée terminée !",
	forceSyncCompleteMessage: "{{count}} jeux mis à jour. REDÉMARREZ STEAM pour voir les changements.",
	syncCancelled: "Synchronisation annulée",
	authTimeout: "Délai de connexion dépassé",
	authTimeoutMessage: "La connexion à {{store}} a expiré. Veuillez réessayer.",
	authFailed: "Échec de l'authentification",
	authFailedMessage: "Impossible de démarrer l'authentification",
	authError: "Erreur d'authentification",
	authConnected: "{{store}} connecté !",
	authConnectedMessage: "Authentification réussie auprès de {{store}}",
	cleanupStarted: "Nettoyage démarré",
	cleanupStartedMessage: "Suppression des raccourcis, des images et des identifiants enregistrés. Cela peut prendre un moment…",
	cleanupSuccessful: "Nettoyage réussi",
	cleanupSuccessfulMessage: "{{games}} raccourcis et {{artwork}} images de couverture supprimés, déconnexion de {{stores}} boutiques effectuée, {{files}} répertoires d'installation supprimés. Cache effacé.",
	cleanupNoop: "Rien à nettoyer",
	cleanupNoopMessage: "Unifideck n'a aucun raccourci, image de couverture ou identifiant enregistré à supprimer.",
	deleteFailed: "Échec de la suppression",
	repairPrefixSuccess: "Préfixe réparé",
	repairPrefixSuccessMessage: "Le préfixe Ubisoft Connect a été reconstruit",
	repairPrefixFailed: "Échec de la réparation",
	repairPrefixFailedMessage: "Impossible de réparer le préfixe : {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Erreur réseau",
		launchRetry: "Nouvelle tentative de lancement",
		retryingUmu: "Nouvelle tentative UMU dans {{seconds}}s (tentative {{attempt}}/{{max}})...",
		protonUpgrade: "Mise à niveau de Proton",
		resettingPrefix: "Réinitialisation du préfixe pour {{version}}...",
		firstTimeSetup: "Première configuration",
		initializingPrefix: "Initialisation du préfixe Wine...",
		setupCompleteTitle: "Configuration terminée",
		prefixInitialized: "Préfixe Wine initialisé",
		setupFallback: "Configuration",
		fallbackInitialization: "Initialisation de secours...",
		launchingGame: "Lancement du jeu",
		startingEpicGame: "Démarrage du jeu Epic...",
		startingAmazonGame: "Démarrage du jeu Amazon...",
		startingGogGame: "Démarrage du jeu GOG...",
		startingUbisoftGame: "Démarrage du jeu Ubisoft...",
		dependenciesTitle: "Installation des dépendances",
		installingRedistributables: "Installation des composants logiciels requis...",
		dependenciesReady: "Dépendances prêtes",
		redistributablesInstalled: "Composants logiciels installés",
		dependenciesStatus: "Dépendances",
		checkLogs: "Configuration terminée (consultez les journaux pour les détails)",
		installingProton: "Installation de Proton",
		attemptingInstall: "Tentative d'installation de {{version}}...",
		epicAuthRequired: "Connexion Epic requise",
		epicAuthRequiredMessage: "Veuillez vous connecter à Epic Games dans les paramètres d'Unifideck",
		installingRedist: "Installation des composants logiciels",
		installingRedistMessage: "Première installation des composants logiciels GOG...",
		setupFailed: "Échec de la configuration",
		setupFailedMessage: "L'installation a échoué, consultez les journaux pour plus de détails",
		prerequisitesTitle: "Installation des prérequis",
		installingPrerequisites: "Installation des prérequis du jeu...",
		prerequisitesReady: "Prérequis prêts",
		prerequisitesInstalled: "Prérequis du jeu installés",
		prerequisitesFailed: "Avertissement sur les prérequis",
		prerequisitesFailedMessage: "Les prérequis n'ont peut-être pas été installés correctement",
		browserRequired: "Microsoft Edge requis",
		signingIn: "Connexion...",
		cloudSyncDownFailed: "Échec du téléchargement des sauvegardes cloud pour {{store}} ({{error}}). Jeu avec sauvegardes locales.",
		cloudSyncUpFailed: "Échec de l'envoi des sauvegardes cloud pour {{store}} ({{error}}). Votre progression est uniquement sauvegardée localement.",
		errorCircuitBreakerOpen: "{{game_key}} a échoué à démarrer {{count}} fois récemment. Suspensions des lancements automatiques.",
		launcherError: "Impossible de lancer {{game_key}} ({{error_code}}). Consultez les journaux pour les détails.",
		downloadingProton: "Téléchargement de Proton — cela peut prendre quelques minutes...",
		downloadingRuntime: "Téléchargement du Steam Runtime — le premier lancement peut prendre quelques minutes...",
		installingUbisoft: "Installation via Ubisoft Connect",
		installingUbisoftMessage: "Ouverture d'Ubisoft Connect — démarrez l'installation depuis la fenêtre du lanceur.",
		protonReadyBody: "{{version}} est prêt à l'emploi",
		protonReadyTitle: "Proton prêt",
		protonSwitchedTo: "Utilisation de {{version}} désormais",
		signingInUbisoft: "Connexion à Ubisoft Connect",
		signingInUbisoftMessage: "Ouverture d'Ubisoft Connect — connectez-vous, puis revenez.",
		ubisoftPrefixNotReady: "Ubisoft Connect n'est pas prêt",
		ubisoftPrefixNotReadyMessage: "Impossible de préparer le préfixe Ubisoft Connect pour ce jeu. Réessayez l'installation ; si l'échec persiste, relancez une synchronisation de la bibliothèque.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect n'est pas prêt",
		ubisoftAuthPrefixNotReadyMessage: "Impossible de préparer Ubisoft Connect pour la connexion. Veuillez réessayer dans un instant.",
		ubisoftLaunchIdMissing: "Ouverture d'Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "Impossible de résoudre l'ID de lancement de ce jeu — ouverture d'Ubisoft Connect pour que vous puissiez le démarrer depuis votre bibliothèque. Relancez une synchronisation de la bibliothèque pour corriger le lancement direct."
	},
	updateComplete: "Mise à jour terminée !",
	updateCompleteMessage: "{{title}} a été mis à jour.",
	updateCancelled: "Mise à jour annulée",
	updateCancelledMessage: "La mise à jour de {{title}} a été annulée.",
	updateFailed: "Échec de la mise à jour",
	updateFailedMessage: "Impossible de démarrer la mise à jour.",
	storeError: "Impossible de communiquer avec {{store}} ({{errType}})",
	captureLogsStarted: "Collecte des journaux...",
	captureLogsStartedMessage: "Création d'une archive de diagnostic dans votre dossier Téléchargements. Cela prend quelques secondes.",
	captureLogsDone: "Journaux capturés",
	captureLogsDoneMessage: "Enregistré dans {{path}} ({{size}}, {{files}} fichiers).",
	captureLogsPartial: "Journaux capturés (partiel)",
	captureLogsPartialMessage: "Enregistré dans {{path}} ({{size}}, {{files}} fichiers). {{failures}} fichier(s) n'ont pas pu être lus.",
	captureLogsFailed: "Impossible de capturer les journaux",
	captureLogsBusy: "Une capture de journaux est déjà en cours",
	captureLogsCopied: "Chemin copié",
	captureLogsCopyFailed: "Impossible de copier le chemin",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Libérer de l'espace",
		signInToStore: "Se connecter",
		retrySync: "Réessayer",
		retryLibrarySync: "Actualiser la bibliothèque",
		openSaveFolder: "Dossier des sauvegardes",
		showLogs: "Afficher les journaux"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Impossible de rafraîchir la bibliothèque {{store}} ({{error}}). Votre bibliothèque existante reste visible."
	},
	cloudPullStarted: "Téléchargement de la sauvegarde cloud…",
	cloudPushStarted: "Téléversement de la sauvegarde locale…",
	cloudSavePathFailed: "Impossible de définir l'emplacement de sauvegarde",
	cloudSavePathSet: "Emplacement de sauvegarde mis à jour",
	cloudPullDone: "Sauvegarde cloud téléchargée",
	cloudPullFailed: "Échec du téléchargement de la sauvegarde cloud",
	cloudPushDone: "Sauvegarde locale téléversée",
	cloudPushFailed: "Échec du téléversement de la sauvegarde locale"
};
var uninstallModal$e = {
	title: "Confirmer la désinstallation",
	description: "Êtes-vous sûr de vouloir désinstaller {{title}} ?",
	deleteProtonLabel: "Supprimer aussi les fichiers Proton",
	deleteProtonDescription: "Supprime les données de compatibilité et les fichiers utilisateur",
	warningTitle: "Avertissement :",
	warningBody: "Cela supprimera définitivement les données de sauvegarde situées dans le préfixe Wine. Assurez-vous d'avoir une sauvegarde.",
	cancel: "Annuler",
	uninstall: "Désinstaller"
};
var languageSettings$e = {
	title: "LANGUE",
	label: "Langue d'affichage",
	description: "Langue de l'interface et du téléchargement des jeux (si disponible)",
	autoDetect: "Auto (Système)"
};
var errors$e = {
	legendaryNotFound: "Outil Epic Games (Legendary) introuvable",
	gogdlNotFound: "Outil GOG (gogdl) introuvable",
	nileNotFound: "Outil Amazon Games (Nile) introuvable",
	authUrlFailed: "Impossible d'obtenir l'URL d'authentification",
	authFailed: "Échec de l'authentification",
	notAuthenticated: "Non authentifié. Veuillez d'abord vous connecter.",
	syncInProgress: "Une synchronisation est déjà en cours",
	syncCancelled: "Synchronisation annulée par l'utilisateur",
	gameNotFound: "Jeu introuvable",
	gameNotInstalled: "Le jeu n'est pas installé",
	installFailed: "L'installation a échoué — consultez les journaux pour les détails",
	installNoDirectory: "Installation terminée mais impossible de localiser le dossier du jeu",
	uninstallFailed: "Échec de la désinstallation",
	lockConflict: "Conflit de verrouillage — veuillez réessayer",
	unknownStore: "Boutique inconnue",
	shortcutWriteFailed: "Échec de l'écriture des raccourcis",
	backgroundSyncDisabled: "Synchronisation en arrière-plan désactivée",
	steamGridDbUnavailable: "Bibliothèque SteamGridDB indisponible",
	deleteSyncInProgress: "Impossible de supprimer pendant une synchronisation",
	alreadyInQueue: "Déjà dans la file d'attente",
	download: {
		diskSpace: "Espace disque insuffisant",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Erreur réseau — vérifiez votre connexion",
		authExpired: "Session expirée — veuillez vous connecter à nouveau",
		toolMissing: "Outil de téléchargement requis introuvable",
		processFailed: "Échec du téléchargement — veuillez réessayer",
		directoryNotFound: "Impossible de localiser les fichiers installés",
		lockConflict: "Une autre opération est en cours — réessayez",
		ubisoftClient: "Erreur du client Ubisoft Connect — essayez de réparer le préfixe",
		ubisoftClientNotRunning: "Le client Ubisoft Connect ne répond pas",
		ubisoftBootstrap: "Impossible de configurer le préfixe Ubisoft Connect",
		ubisoftUmuMissing: "umu-run introuvable — requis pour les jeux Ubisoft",
		generic: "L'installation a échoué — consultez les journaux pour les détails"
	},
	unknown: "Erreur inconnue",
	steamAppsUnavailable: "API SteamClient.Apps indisponible"
};
var sync$e = {
	start: "Synchroniser les bibliothèques",
	force: "Forcer la synchronisation",
	syncing: "Synchronisation…",
	cancel: "Annuler la synchronisation",
	cancelling: "Annulation…",
	preparing: "Préparation…",
	downloadingArtwork: "Téléchargement de l'image pour {{game}}...",
	fetchingGameLists: "Récupération des listes de jeux...",
	fetchingStore: "Récupération de {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Synchronisation annulée par l'utilisateur",
	checkingInstalledGames: "Vérification des jeux installés...",
	sgdbLookup: "Recherche de {{count}} jeux sur SteamGridDB...",
	extractingSteamMetadata: "Extraction des métadonnées Steam...",
	fetchingEnhancedMetadata: "Récupération des métadonnées enrichies...",
	lookingUpUnifiDB: "Récupération des métadonnées unifiDB...",
	fetchingMetacriticData: "Récupération des métadonnées Metacritic...",
	fetchingCompatData: "Vérification de la compatibilité...",
	checkingExistingArtwork: "Vérification des images de couverture existantes...",
	cleaningOrphanedArtwork: "Nettoyage des images orphelines...",
	retryingMissingArtwork: "Nouvelle tentative pour {{synced}} / {{total}} jeux sans image...",
	savingShortcuts: "Enregistrement des raccourcis...",
	completed: "Synchronisation terminée !",
	extractingMetadata: "Extraction des métadonnées pour {{game}}...",
	cooldown: "Patientez {{secs}}s",
	artworkPhase: "Image {{done}} / {{total}}"
};
var force_sync$e = {
	migratingOldInstallations: "Synchro forcée : Migration des anciennes installations...",
	fetchingGameLists: "Synchro forcée : Récupération des listes de jeux...",
	checkingInstalledGames: "Synchro forcée : Vérification des jeux installés...",
	rewritingShortcuts: "Synchro forcée : Réécriture des raccourcis...",
	cancelledShortcutsSaved: "Synchro forcée annulée - raccourcis sauvegardés",
	writingShortcuts: "Synchro forcée : Écriture des raccourcis...",
	clearingProton: "Synchro forcée : Nettoyage de la compatibilité Proton...",
	completed: "Synchro forcée terminée ! {{updated}} jeux mis à jour, {{artwork}} images récupérées."
};
var artwork$e = {
	checking: "Vérification des images de couverture...",
	queueRefresh: "Mise en file d'attente pour actualiser les images...",
	downloadProgress: "Images de couverture téléchargées : {{synced}}/{{total}} ({{game}})"
};
var deckTabs$e = {
	greatOnDeck: "Validé sur Deck",
	allGames: "Tous les jeux",
	installed: "Installé",
	nonSteam: "Non Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$e = {
	error: "Erreur Unifideck",
	errorLoadingView: "Impossible de charger la vue bibliothèque unifiée",
	checkConsole: "Consultez la console du navigateur pour plus de détails",
	errorLoadingGames: "Erreur lors du chargement des jeux",
	tryReloading: "Essayez de recharger le plugin ou de vérifier la console",
	storeLabel: "Boutique :",
	allStores: "Toutes les boutiques",
	searchPlaceholder: "Rechercher des jeux...",
	library: "Bibliothèque"
};
var gameInfoPanel$e = {
	loading: "Chargement des informations du jeu...",
	noMetadata: "Aucune métadonnée disponible pour ce jeu.",
	noTestResults: "Aucun résultat de test détaillé disponible.",
	buttons: {
		details: "Détails",
		synopsis: "Synopsis",
		storePage: "Page du magasin",
		dlc: "DLC",
		communityHub: "Hub de la communauté",
		pointsShop: "Boutique des points",
		discussions: "Discussions",
		guides: "Guides",
		support: "Support",
		install: "Installer",
		uninstall: "Désinstaller",
		cancel: "Annuler",
		achievements: "Succès"
	},
	labels: {
		store: "Boutique :",
		name: "Nom :",
		size: "Taille :",
		developer: "Développeur :",
		publisher: "Éditeur :",
		released: "Sortie :",
		metacritic: "Metacritic :",
		status: "Statut :",
		spaceRequired: "Espace requis",
		lastPlayed: "Dernière partie",
		playTime: "Temps de jeu",
		never: "Jamais"
	},
	compatibility: {
		modalTitle: "Compatibilité Steam Deck",
		viewOnProtonDb: "Voir sur ProtonDB",
		close: "Fermer",
		verified: "COMPATIBLE",
		playable: "JOUABLE",
		unsupported: "NON SUPPORTÉ",
		unknown: "INCONNU"
	},
	failedToLoadMetadata: "Échec du chargement des métadonnées",
	lastSession: "Dernière session : {{names}} débloqué(s)"
};
var gameDetailsSettings$e = {
	title: "VUE DU JEU",
	simple: "Simple",
	detailed: "Détaillée"
};
var gogLanguageModal$e = {
	title: "Sélectionner la langue",
	description: "Choisissez la langue à télécharger pour {{title}}",
	label: "Langue",
	install: "Installer",
	cancel: "Annuler"
};
var accountSwitch$e = {
	title: "Changement de compte détecté",
	description: "Un autre compte Steam est actuellement connecté. Que souhaitez-vous faire des données Unifideck ?",
	migrate: "Migrer les données Unifideck",
	migrateDescription: "Copier les jeux et images du compte précédent",
	freshStart: "Nouvelle installation",
	freshStartDescription: "Effacer les identifiants pour connecter d'autres comptes Epic/GOG/Amazon/Ubisoft",
	skip: "Passer",
	toastMigrateTitle: "Migration terminée",
	toastMigrateBody: "{{shortcuts}} raccourcis et {{artwork}} illustrations migrés",
	toastClearTitle: "Jetons d'authentification effacés",
	toastClearBody: "Les identifiants de boutique ont été effacés. Vous pouvez maintenant vous connecter avec d'autres comptes."
};
var ubisoftAuth$e = {
	title: "Connexion Ubisoft Connect",
	description: "Connectez-vous via Ubisoft Connect pour associer votre bibliothèque.",
	signInViaUpc: "Se connecter via Ubisoft Connect",
	upcDescription: "Ubisoft Connect va s'ouvrir pour vous permettre de vous connecter. Une fois connecté, votre session sera automatiquement capturée.",
	launchingUpc: "Lancement de Ubisoft Connect...",
	waitingForLogin: "En attente de connexion...",
	retryLaunch: "Réessayer",
	cancel: "Annuler",
	launchFailed: "Impossible de lancer Ubisoft Connect. Veuillez réessayer.",
	signingIn: "Connexion en cours...",
	emailLabel: "E-mail",
	passwordLabel: "Mot de passe",
	signIn: "Se connecter",
	twoFactorTitle: "Authentification à deux facteurs",
	twoFactorDescription: "Entrez le code de vérification généré par votre application d'authentification.",
	twoFactorLabel: "Code de vérification",
	verify: "Vérifier",
	verifying: "Vérification...",
	errorEmptyFields: "Veuillez entrer votre e-mail et votre mot de passe",
	errorEmpty2FA: "Veuillez entrer le code de vérification",
	errorLoginFailed: "Échec de la connexion. Veuillez vérifier vos identifiants.",
	errorInvalid2FA: "Code de vérification invalide. Veuillez réessayer.",
	showPassword: "Afficher le mot de passe",
	hidePassword: "Masquer le mot de passe",
	doubleLoginWarning: "Vous devrez vous connecter deux fois pour Ubisoft."
};
var relativeDate$e = {
	today: "Aujourd'hui",
	yesterday: "Hier"
};
var common$e = {
	thisGame: "ce jeu",
	game: "Jeu",
	unknown: "Inconnu",
	ok: "OK",
	cancel: "Annuler",
	working: "Traitement…",
	loading: "Chargement…",
	free: "gratuit",
	close: "Fermer"
};
var info$e = {
	expandDetails: "Afficher les détails",
	collapseDetails: "Masquer les détails"
};
var microsoft$e = {
	notCompatible: "Ce format de jeu n'est pas compatible avec SteamOS",
	playOnCloud: "Jouer sur le Cloud",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Connectez-vous avec votre compte Microsoft / Xbox",
	accountConnected: "Compte Microsoft connecté",
	loggedOut: "Déconnecté du Microsoft Store",
	xcloudNotInstalled: "Les jeux xCloud ne sont pas installés localement",
	chromiumRequired: "Microsoft Edge requis",
	chromiumRequiredMessage: "La connexion nécessite Microsoft Edge. Souhaitez-vous l'installer automatiquement ? (Cette opération n'est requise qu'une seule fois.)",
	chromiumInstallButton: "Installer Microsoft Edge",
	chromiumInstalling: "Installation de Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge installé !",
	chromiumAlreadyInstalled: "Un navigateur compatible est déjà installé",
	chromiumInstallFailed: "Échec de l'installation de Microsoft Edge. Veuillez l'installer manuellement : flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "L'installation de Microsoft Edge a expiré. Veuillez vérifier votre connexion Internet.",
	browserAlreadyInstalled: "Un navigateur compatible est déjà installé",
	browserInstalled: "Microsoft Edge installé avec succès !",
	browserInstallFailed: "Échec de l'installation de Microsoft Edge. Veuillez l'installer manuellement : flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak n'est pas disponible sur ce système",
	signInRequired: "Connectez-vous pour jouer",
	syncSkippedNoSubscription: "Aucun abonnement Xbox Game Pass actif détecté. Votre bibliothèque xCloud n'a pas été synchronisée.",
	syncSkippedTierUnknown: "Votre abonnement Xbox Game Pass a été détecté mais le niveau d'abonnement n'a pas pu être déterminé. Synchronisation ignorée pour éviter d'afficher des jeux indisponibles.",
	subscriptionCheckFailed: "Impossible de vérifier votre abonnement Xbox Game Pass. Synchronisation xCloud ignorée. Réessayez lorsque votre connexion sera stable.",
	subscriptionDetected: "Abonnement Xbox Game Pass {{tier}} actif."
};
var playButton$e = {
	play: "Jouer",
	install: "Installer",
	cancel: "Annuler",
	retry: "Réessayer",
	downloadFailed: "Téléchargement échoué",
	installing: "Installation",
	controllerConfig: "Configuration du contrôleur",
	appSettings: "Paramètres de l'application"
};
var play$e = {
	play: "Jouer",
	resume: "Reprendre",
	stop: "Arrêter",
	cancel: "Annuler",
	cancelling: "Annulation…",
	update: "Mettre à jour",
	uninstall: "Désinstaller",
	queued: "En file d'attente",
	downloading: "Téléchargement",
	extracting: "Extraction",
	verifying: "Vérification",
	finalizing: "Finalisation de l'installation",
	cancelConfirmTitle: "Annuler le téléchargement ?",
	cancelConfirmBody: "Êtes-vous sûr de vouloir annuler le téléchargement de {{title}} ? Les fichiers partiels seront supprimés.",
	cancelConfirmConfirm: "Annuler le téléchargement",
	cancelConfirmCancel: "Poursuivre le téléchargement",
	playOnCloud: "Jouer sur le Cloud",
	cloudSave: {
		autoPullOn: "Les sauvegardes cloud se téléchargent automatiquement au lancement.",
		close: "Fermer",
		download: "Télécharger la sauvegarde cloud",
		filesCount: "{{count}} fichiers",
		label: "Sauvegardes cloud",
		menuTitle: "Sauvegardes cloud — {{game}}",
		noCloudData: "Inconnu",
		noCloudSupport: "Pas de prise en charge des sauvegardes cloud",
		setLocation: "Définir l'emplacement de sauvegarde",
		setLocationHint: "Choisissez le dossier où ce jeu stocke ses sauvegardes.",
		setLocationTitle: "Choisir le dossier de sauvegarde de ce jeu",
		statusCloudAvailable: "Sauvegarde cloud disponible",
		statusNoSaves: "Pas de sauvegardes locales",
		statusSyncing: "Synchronisation des sauvegardes cloud…",
		statusUnresolved: "Emplacement de sauvegarde introuvable — la synchronisation pourrait ne pas fonctionner pour ce jeu.",
		unresolvedHint: "Lancez le jeu une fois pour que nous puissions détecter son dossier de sauvegarde, ou définissez l'emplacement manuellement.",
		upload: "Téléverser la sauvegarde locale"
	},
	exe: {
		browse: "Parcourir…",
		changeFailed: "Impossible de modifier l'exécutable",
		changed: "Exécutable modifié",
		defaultTag: "(par défaut)",
		menuItem: "Modifier l'exécutable…",
		reset: "Réinitialiser à l'exécutable par défaut",
		resetButton: "Réinitialiser",
		subtitle: "Choisissez le fichier avec lequel lancer ce jeu (ex. contourner un lanceur et lancer le jeu directement). Le dossier de travail et les sauvegardes cloud ne sont pas affectés.",
		title: "Modifier l'exécutable — {{game}}",
		unavailable: "Impossible de lire le dossier de ce jeu."
	}
};
var playMeta$e = {
	spaceRequired: "Espace requis",
	installedSize: "Taille installée",
	lastPlayed: "Dernière partie",
	neverPlayed: "Jamais joué",
	hoursShort: "h",
	minutesShort: "m",
	played: "Jouées"
};
var downloads$e = {
	title: "Téléchargements",
	empty: "Aucun téléchargement. Installez des jeux depuis votre bibliothèque pour les voir ici.",
	current: "Téléchargement en cours",
	queued: "En file d'attente",
	finished: "Récemment terminés",
	cancel: "Annuler",
	removeFromQueue: "Supprimer",
	play: "Jouer",
	outcome: {
		queued: "En file d'attente",
		downloading: "Téléchargement",
		completed: "Terminé",
		cancelled: "Annulé",
		error: "Échoué"
	}
};
var navigation$e = {
	loading: "Chargement...",
	storesTitle: "Boutiques",
	syncTitle: "Sync de la bibliothèque",
	languageTitle: "Langue"
};
var security$e = {
	panelTitle: "Sécurité",
	counters: {
		label: "Compteurs",
		empty: "Aucun événement observé depuis le démarrage"
	},
	log: {
		label: "Journal d'audit",
		entryCount: "{{count}} entrée",
		entryCount_other: "{{count}} entrées",
		emptyFiltered: "Aucun événement ne correspond au filtre"
	},
	filter: {
		label: "Filtre",
		all: "Tous les événements"
	},
	timeline: {
		label: "Chronologie 24h ({{count}} événement)",
		label_other: "Chronologie 24h ({{count}} événements)",
		axisStart: "-24h",
		axisEnd: "maintenant"
	},
	relativeTime: {
		seconds: "il y a {{count}}s",
		minutes: "il y a {{count}} min",
		hours: "il y a {{count}}h",
		days: "il y a {{count}}j"
	},
	bruteforce: {
		warningTitle: "Avertissement : détection de force brute (niveau alerte jaune)",
		escalationTitle: "ALERTE : détection de force brute (niveau alerte rouge)",
		detail: "{{failures}} échecs de déchiffrement au cours des dernières {{window}}s. Seuils : {{warning}} (avertissement) / {{escalation}} (escalade).",
		resetButton: "Prendre acte et réinitialiser le détecteur"
	},
	actions: {
		clearLog: "Effacer le journal et les compteurs"
	},
	events: {
		tokenEncrypted: "Jeton chiffré",
		tokenDecrypted: "Jeton déchiffré",
		decryptFailed: "Échec du déchiffrement",
		tokenFileMigrated: "Fichier de jetons migré",
		legacyPlaintextDetected: "Texte brut hérité détecté",
		authFlowStarted: "Authentification lancée",
		authFlowCompleted: "Authentification réussie",
		authFlowFailed: "Authentification échouée",
		permissionsCheck: "Autorisations vérifiées",
		permissionsRepaired: "Autorisations réparées",
		bruteforceSuspected: "Suspicion de force brute",
		deviceResetDetected: "Réinitialisation de l'appareil détectée",
		fingerprintInitialized: "Empreinte numérique initialisée",
		externalAuthCheckFailed: "Échec de la vérification d'authentification externe",
		configValidationCompleted: "Validation de configuration réussie",
		configValidationFailed: "Échec de validation de configuration"
	},
	configValidation: {
		degradedTitle: "Configuration invalide - mode dégradé actif",
		detail: "{{count}} erreur de validation détectée au démarrage",
		detail_other: "{{count}} erreurs de validation détectées au démarrage",
		sourceDefaults: "valeurs par défaut",
		sourceUser: "modifications utilisateur",
		sourceMerged: "fusionnée",
		overflow: "+ {{count}} autre erreur",
		overflow_other: "+ {{count}} autres erreurs"
	}
};
var cloudSyncBehavior$e = {
	_comment: "Panneau de configuration permettant à l'utilisateur de choisir, par boutique, comment les erreurs de synchronisation cloud doivent être signalées. Les cinq entrées de store_label correspondent aux lignes affichées par CloudSyncBehaviorPanel.tsx. Microsoft est délibérément omis — le streaming xCloud n'a pas de sauvegarde locale à synchroniser.",
	title: "Comportement de sync cloud",
	description: "Choisissez comment Unifideck doit réagir lorsqu'un téléchargement ou téléversement de sauvegarde cloud échoue au lancement du jeu.",
	loadError: "Impossible de charger les paramètres actuels",
	saveError: "Impossible d'enregistrer vos modifications — veuillez réessayer",
	store_label: {
		"default": "Par défaut (toutes les boutiques)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Silencieux (journal uniquement)",
		toast: "Notifier (notification)"
	}
};
var cloudSync$e = {
	_comment: "Chaînes de caractères de notification émises par le lanceur lorsqu'une synchronisation cloud échoue. Le paramètre d'erreur est résolu via cloudSync.error.<code> en fonction du champ error_i18n_key de la réponse du backend.",
	error: {
		network_unreachable: "Réseau inaccessible",
		auth_expired: "Authentification expirée",
		forbidden: "Accès refusé",
		quota_exceeded: "Quota cloud dépassé",
		server_error: "Serveur de la boutique indisponible",
		timed_out: "La demande a expiré",
		disk_full: "Espace disque insuffisant",
		disk_space_low: "Ignoré — espace disque faible (libérez de l'espace et réessayez)",
		permission_denied: "Permission refusée",
		cancelled: "Annulé",
		unknown: "Erreur inattendue"
	},
	unavailableNativeDep: "Les sauvegardes cloud sont désactivées sur ce système — son Python ({{python}}) ne dispose pas d'un composant requis. Vos sauvegardes locales ne sont pas affectées ; recréez/réinstallez le plugin après une mise à jour du système pour restaurer la synchronisation."
};
var launchLogs$e = {
	_comment: "Chaînes pour LaunchLogsModal. Le modal s'ouvre à partir de l'action de notification unifideck://show-logs/<launch_id>, associée aux notifications d'échec du lanceur (circuit breaker + LauncherError). Lit les journaux archivés par lancement depuis ~/.local/share/unifideck/launches/<launch_id>.log et les affiche avec un code couleur basé sur le niveau.",
	title: "Journaux de lancement — {{launchId}}",
	close: "Fermer",
	copyAll: "Copier tout",
	"export": "Exporter dans un fichier",
	copied: "Copié dans le presse-papiers",
	copyFailed: "Copie échouée",
	exporting: "Exportation…",
	exportedTo: "Exporté vers {{path}}",
	exportFailed: "Échec de l'exportation : {{error}}",
	loading: "Chargement des journaux…",
	notAvailable: "Les journaux de ce lancement ne sont plus disponibles (au-delà de la période de conservation de 7 jours).",
	error: "Impossible de lire les journaux : {{error}}",
	fileInfo: "{{path}} — affichage de {{shown}} sur {{total}} lignes",
	filterLabel: "Afficher",
	filterInfo: "Info",
	filterWarning: "Avertissement",
	filterError: "Erreur",
	nextError: "Erreur suivante ({{current}}/{{total}})",
	noErrors: "Aucune erreur",
	allFiltered: "Toutes les lignes sont masquées par le filtre actuel. Activez au moins un niveau pour voir les entrées du journal."
};
var settings$e = {
	_comment: "Contrôles du panneau UnifideckSettingsPanel qui ne sont pas couverts par des composants plus spécifiques (LanguageSelector, CloudSyncBehaviorPanel). refreshAll is un bouton qui déclenche sync_all() sur toutes les boutiques enregistrées ; la vue Bibliothèque affiche la progression de la synchronisation via son propre abonnement aux événements SYNC_PROGRESS.",
	storeConnections: "Connexions aux boutiques",
	librarySync: "Sync de la bibliothèque",
	language: "Langue",
	languageAuto: "Détection automatique",
	refreshAll: {
		sectionTitle: "Actions de bibliothèque",
		button: "Actualiser toutes les bibliothèques",
		buttonBusy: "Démarrage…",
		startedTitle: "Actualisation de la bibliothèque démarrée",
		startedBody: "Synchronisation de toutes les boutiques en arrière-plan.",
		startedSingle: "Actualisation de la bibliothèque {{store}} en arrière-plan.",
		failedTitle: "Impossible de démarrer l'actualisation de la bibliothèque"
	}
};
var saveFolder$e = {
	_comment: "Chaînes pour SaveFolderModal. Le modal s'ouvre à partir de l'action de notification unifideck://open-save-folder/<store>/<game_id>, généralement affichée lors des échecs de synchronisation cloud lorsque l'utilisateur souhaite inspecter ce qui est réellement stocké localement. count et size sont interpolés ; truncated s'affiche uniquement lorsque le résultat a été limité.",
	title: "Dossier de sauvegarde — {{store}} / {{gameId}}",
	close: "Fermer",
	copyPath: "Copier le chemin",
	copyFileList: "Copier la liste des fichiers",
	depthLabel: "Profondeur",
	filterLabel: "Filtre",
	loading: "Chargement du contenu du dossier…",
	noSavesYet: "Aucune sauvegarde locale trouvée pour ce jeu pour le moment. Le dossier sera créé lors de la prochaine synchronisation cloud réussie.",
	aggregate: "{{count}} fichiers, {{size}} au total",
	truncated: "{{count}} fichiers supplémentaires ({{size}}) non affichés — affinez votre filtre pour les voir.",
	error: "Impossible de lire le dossier de sauvegarde : {{error}}"
};
var library$e = {
	_comment: "Fonctionnalités de l'interface utilisateur par jeu affichées dans la vue Bibliothèque, y compris le badge du disjoncteur et les commandes de lancement forcé / de réinitialisation lorsque les lancements échouent de manière répétée.",
	circuitBreaker: {
		_comment: "Interface pour le disjoncteur de lancement échoué. Le badge apparaît uniquement lorsque les échecs dans la fenêtre configurée atteignent circuit_breaker.failures_threshold (par défaut 3 en 600s). Le lancement forcé active un contournement unique ; la réinitialisation efface complètement l'historique des échecs.",
		badgeRecent: "{{count}} échec de lancement récent",
		badgeRecent_other: "{{count}} échecs de lancement récents",
		badgeTooltip: "{{count}} lancements échoués dans la période récente — le disjoncteur a suspendu les tentatives automatiques",
		forceLaunchButton: "Forcer le lancement",
		resetButton: "Réinitialiser les échecs",
		failureKind: {
			fastBoot: "Crash au démarrage rapide",
			launcherError: "Erreur d'initialisation du lancement"
		}
	}
};
var pickStorage$e = {
	title: "Emplacement d'installation",
	customOption: "Choisir un autre emplacement...",
	customLabel: "Emplacement personnalisé",
	change: "Modifier...",
	gameSize: "Taille d'installation"
};
var restart$e = {
	title: "Synchronisation terminée !",
	body: "Veuillez redémarrer Steam pour appliquer vos nouveaux jeux et images de couverture",
	confirm: "Redémarrer maintenant",
	later: "Plus tard"
};
var updater$e = {
	checkButton: "Rechercher des mises à jour",
	checkCompleteMessage: "Informations de version récupérées avec succès.",
	checkCompleteTitle: "Recherche de mise à jour terminée",
	checkFailedTitle: "Échec de la recherche",
	checkingButton: "Recherche...",
	currentTitle: "Actuelle",
	downgradeButton: "Rétrograder vers la v{{version}}",
	installFailedTitle: "Échec de l'installation",
	installTimeoutMessage: "Aucune réponse de Decky Loader — l'installation a peut-être bloqué ou échoué. Veuillez réessayer.",
	installedLabel: "installée",
	installingButton: "Installation...",
	installingTitle: "Installation du plugin",
	latestLabel: "dernière",
	modalTitle: "UNIFIDECK v{{version}} — Notes de version",
	noBackend: "Le backend Decky est indisponible.",
	noReleaseNotes: "Aucune note de version disponible.",
	reinstallButton: "Réinstaller la v{{version}}",
	refreshFailedMessage: "Impossible de vérifier les informations de la dernière version. Vérifiez votre connexion et réessayez.",
	releaseGoneMessage: "Cette version n'est plus disponible. Veuillez rechercher des mises à jour et la sélectionner à nouveau.",
	releaseNotesButton: "Notes de version",
	stageDownload: "Téléchargement…",
	stageFinish: "Finalisation…",
	stageInstall: "Installation…",
	stageOpen: "Lecture du paquet…",
	stageParse: "Vérification…",
	stageRemove: "Suppression de l'ancienne version…",
	stageStart: "Démarrage…",
	stageWorking: "Installation…",
	titleLoading: "Vérification de la version",
	typeDowngrade: "Rétrogradation vers",
	typeReinstall: "Réinstallation",
	typeUpdate: "Mise à jour vers",
	updateButton: "Mettre à jour vers la v{{version}}"
};
var achievements$e = {
	unlockedTitle: "Succès débloqué",
	unlockedBody: "{{name}}"
};
var achievementsModal$e = {
	title: "Succès",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Actualiser",
	close: "Fermer",
	loading: "Chargement des succès…",
	empty: "Ce jeu n'a aucun succès.",
	hidden: "Succès caché",
	unlockedOn: "Débloqué le {{date}}",
	error: "Impossible de charger les succès.",
	errorOffline: "Connectez-vous à Internet pour voir les succès.",
	errorAuth: "Connectez-vous à la boutique de ce jeu pour voir les succès.",
	errorUnavailable: "Les succès ne sont pas disponibles pour ce jeu."
};
var collectionSettings$e = {
	title: "COLLECTIONS STEAM",
	enabled: "Collections activées",
	disabled: "Collections désactivées"
};
var frFR = {
	_comment: _comment$e,
	tabs: tabs$e,
	auth: auth$e,
	storage: storage$e,
	storageSettings: storageSettings$e,
	gameGrid: gameGrid$e,
	downloadsTab: downloadsTab$e,
	cloudSave: cloudSave$e,
	storeConnections: storeConnections$e,
	librarySync: librarySync$e,
	cleanup: cleanup$e,
	captureLogs: captureLogs$e,
	installButton: installButton$e,
	confirmModals: confirmModals$e,
	toasts: toasts$e,
	uninstallModal: uninstallModal$e,
	languageSettings: languageSettings$e,
	errors: errors$e,
	sync: sync$e,
	force_sync: force_sync$e,
	artwork: artwork$e,
	deckTabs: deckTabs$e,
	unifiedLibrary: unifiedLibrary$e,
	gameInfoPanel: gameInfoPanel$e,
	gameDetailsSettings: gameDetailsSettings$e,
	gogLanguageModal: gogLanguageModal$e,
	accountSwitch: accountSwitch$e,
	ubisoftAuth: ubisoftAuth$e,
	relativeDate: relativeDate$e,
	common: common$e,
	info: info$e,
	microsoft: microsoft$e,
	playButton: playButton$e,
	play: play$e,
	playMeta: playMeta$e,
	downloads: downloads$e,
	navigation: navigation$e,
	security: security$e,
	cloudSyncBehavior: cloudSyncBehavior$e,
	cloudSync: cloudSync$e,
	launchLogs: launchLogs$e,
	settings: settings$e,
	saveFolder: saveFolder$e,
	library: library$e,
	pickStorage: pickStorage$e,
	restart: restart$e,
	updater: updater$e,
	achievements: achievements$e,
	achievementsModal: achievementsModal$e,
	collectionSettings: collectionSettings$e
};

var _comment$d = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$d = {
	settings: "Einstellungen",
	downloads: "Downloads"
};
var auth$d = {
	connect: "Verbinden",
	disconnect: "Trennen",
	status: {
		connected: "Verbunden",
		disconnected: "Nicht verbunden",
		checking: "Überprüfen…",
		error: "Fehler — Protokolle prüfen"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$d = {
	title: "Speicher",
	defaultUpdated: "Standardspeicher aktualisiert",
	isDefault: "Standard",
	setDefault: "Als Standard festlegen",
	customPath: "Benutzerdefinierter Pfad",
	up: "↑ Aufwärts",
	refresh: "Aktualisieren",
	useThisPath: "Diesen Pfad verwenden"
};
var storageSettings$d = {
	title: "Download-Einstellungen",
	installLocationLabel: "Installationsort",
	installLocationDescription: "Wohin neue Spiele heruntergeladen werden",
	pathLabel: "Pfad",
	loading: "Speicheroptionen werden geladen...",
	toastUpdatedTitle: "Speicherort aktualisiert",
	toastUpdatedBody: "Neue Spiele werden unter {{location}} installiert",
	toastFailedTitle: "Aktualisierung fehlgeschlagen",
	toastFailedBody: "{{error}}",
	unknown: "Unbekannt",
	browseButton: "Durchsuchen...",
	clearCustom: "Benutzerdefinierten Ort löschen",
	customPathSet: "Benutzerdefinierter Installationsort festgelegt",
	newFolder: "Neuer Ordner",
	newFolderCreate: "Erstellen",
	selectFolder: "Diesen Ordner verwenden",
	emptyDirectory: "Leerer Ordner",
	showHidden: "Versteckte anzeigen",
	selectDevice: "Gerät auswählen",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Geändert (Neueste)",
	sortModifiedOldest: "Geändert (Älteste)",
	sortCreatedNewest: "Erstellt (Neueste)",
	sortCreatedOldest: "Erstellt (Älteste)",
	sortSizeLargest: "Größe (Größte)",
	sortSizeSmallest: "Größe (Kleinste)",
	"validationErrors.notAbsolute": "Pfad muss absolut sein",
	"validationErrors.systemDir": "Systemverzeichnis kann nicht verwendet werden",
	"validationErrors.notWritable": "Ort ist nicht beschreibbar",
	"validationErrors.notFound": "Verzeichnis existiert nicht",
	"validationErrors.homeRoot": "Benutzerverzeichnis-Root kann nicht verwendet werden",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB frei)"
};
var gameGrid$d = {
	loading: "Spiele werden geladen...",
	noGamesFound: "Keine Spiele gefunden",
	noGamesHint: "Versuche, deine Bibliotheken in den Unifideck-Einstellungen zu synchronisieren",
	showingGames: "Zeige {{count}} Spiele",
	installed: "Installiert",
	notInstalled: "Nicht installiert",
	playtime: "{{hours}} Std. {{minutes}} Min. gespielt"
};
var downloadsTab$d = {
	currentDownload: "AKTUELLER DOWNLOAD",
	queuedDownloads: "WARTESCHLANGE ({{count}})",
	recentlyCompleted: "KÜRZLICH ABGESCHLOSSEN",
	noActiveDownloads: "Keine aktiven Downloads",
	noDownloads: "Keine Downloads. Installiere Spiele aus deiner Bibliothek, um sie hier zu sehen.",
	loadingLabel: "Laden...",
	loadingMessage: "Download-Warteschlange wird abgerufen...",
	confirmCancelTitle: "Abbruch bestätigen",
	confirmCancelDescription: "Bist du sicher, dass du den Download von {{game}} abbrechen möchtest?",
	confirmCancelYes: "Ja",
	confirmCancelNo: "Nein",
	cancelDownload: "Abbrechen",
	toastDownloadCancelledTitle: "Download abgebrochen",
	toastDownloadCancelledBody: "Der Download wurde aus der Warteschlange entfernt.",
	toastCancelFailedTitle: "Abbruch fehlgeschlagen",
	toastCancelFailedBody: "Download konnte nicht abgebrochen werden: {{error}}",
	status: {
		queued: "In Warteschlange",
		downloading: "Wird heruntergeladen",
		completed: "Abgeschlossen",
		cancelled: "Abgebrochen",
		error: "Fehler"
	},
	preparingDownload: "Download wird vorbereitet...",
	phaseExtracting: "{{game}} wird entpackt...",
	phaseVerifying: "{{game}} wird überprüft...",
	internalStorage: "Interner Speicher ({{freeSpace}} GB frei)",
	sdCard: "SD-Karte ({{freeSpace}} GB frei)",
	customLocation: "Benutzerdefiniert: {{path}} ({{freeSpace}} GB frei)",
	extractingLabel: "Entpacken...",
	verifyingLabel: "Überprüfen...",
	updateQueuedLabel: "Update eingereiht",
	downloadQueuedLabel: "Download eingereiht",
	downloadingUpdateLabel: "Update wird heruntergeladen",
	downloadingLabel: "Wird heruntergeladen...",
	finalizingInstallation: "Installation wird abgeschlossen...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "Verbleibende Zeit: {{eta}}",
	preparingMessage: "Präfix wird initialisiert und Cloud-Spielstände werden synchronisiert...",
	preparingLabel: "Spiel wird eingerichtet...",
	installingViaUpcLabel: "Installation über Ubisoft Connect",
	extractingMessage: "Dateien werden extrahiert...",
	verifyingMessage: "Dateien werden überprüft... {{pct}}%",
	installingViaUpcMessage: "Folge dem Ubisoft-Connect-Fenster, um die Installation abzuschließen..."
};
var cloudSave$d = {
	title: "Cloud-Speicher-Konflikt",
	description: "Deine lokalen Spielstände weichen für {{game}} von der Cloud ab",
	local: "Lokal:",
	cloud: "Cloud:",
	newer: "(neuer)",
	useCloud: "Cloud verwenden",
	useLocal: "Lokal verwenden",
	unknownDate: "Unbekannt",
	toastUsingCloudTitle: "Cloud-Spielstände werden verwendet",
	toastUsingCloudBody: "Cloud-Spielstände für {{game}} werden heruntergeladen",
	toastUsingLocalTitle: "Lokale Spielstände werden verwendet",
	toastUsingLocalBody: "Lokale Spielstände für {{game}} werden hochgeladen",
	toastErrorTitle: "Fehler",
	toastErrorBody: "Konflikt konnte nicht gelöst werden",
	filesSize: "{{count}} Dateien · {{size}}",
	uploadBlockedEmpty: "Cloud-Upload für {{game}} übersprungen – keine lokalen Spielstände gefunden",
	uploadSkippedTitle: "Cloud-Upload übersprungen",
	uploadSkippedBody: "Noch keine lokalen Spielstände zum Hochladen für {{game}}."
};
var storeConnections$d = {
	title: "STORE-VERBINDUNGEN",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Verbunden",
	checking: "Überprüfen...",
	authenticate: "Anmelden",
	legendaryNotInstalled: "⚠️ Epic Games CLI wird installiert...",
	nileNotInstalled: "⚠️ Amazon Games CLI nicht gefunden",
	error: "Fehler - Protokolle prüfen"
};
var librarySync$d = {
	title: "BIBLIOTHEKS-SYNC",
	syncLibraries: "Bibliotheken synchronisieren",
	forceSync: "Synchronisierung erzwingen",
	syncing: "Synchronisieren...",
	cancelSync: "Synchronisierung abbrechen",
	retryStatusCheck: "Statusprüfung wiederholen",
	gamesSynced: "{{synced}} / {{total}} Spiele synchronisiert",
	artworkDownloaded: "{{synced}} / {{total}} Coverbilder heruntergeladen",
	metadataExtracted: "{{synced}} / {{total}} Metadatensätze extrahiert",
	steamMetadataDownloaded: "Steam-Metadaten: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDB-Metadaten: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacritic-Daten: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWG-Metadaten: {{synced}} / {{total}}",
	compatChecked: "Kompatibilitätsdaten: {{synced}} / {{total}}",
	cancelling: "Wird abgebrochen…",
	error: "Fehler"
};
var cleanup$d = {
	title: "Bereinigung",
	deleteAll: "Alle UNIFIDECK Bibliotheken und Cache löschen",
	modalTitle: "Alle Unifideck-Daten löschen?",
	modalDescription: "Dadurch werden alle Unifideck-Verknüpfungen, Steam-Grafiken, gespeicherten Store-Logins und zwischengespeicherten Daten gelöscht. Installierte Spiele verbleiben auf der Festplatte und können neu synchronisiert werden. Aktiviere unten den destruktiven Modus, um auch die installierten Spieldateien zu löschen.",
	deleteFilesLabel: "Auch installierte Spieldateien löschen (Destruktiv)",
	deleteFilesDescription: "Entfernt das Installationsverzeichnis auf der Festplatte für jedes Unifideck-Spiel unter einem sicheren Root-Pfad.",
	confirmDelete: "Ja, alles löschen",
	cancel: "Abbrechen",
	deleting: "Wird gelöscht..."
};
var captureLogs$d = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (de-DE).",
	title: "Protokolle erfassen",
	description: "Sammelt alle Unifideck-Protokolle in einer ZIP-Datei in Ihrem Downloads-Ordner, damit Sie sie an einen Fehlerbericht anhängen können.",
	capture: "Protokolle erfassen",
	capturing: "Protokolle werden gesammelt...",
	savedTo: "In Ihrem Downloads-Ordner gespeichert",
	bundleInfo: "{{files}} Dateien · {{size}}",
	partialNote: "{{count}} Datei konnte nicht gelesen werden. Das Paket ist dennoch nutzbar.",
	partialNote_other: "{{count}} Dateien konnten nicht gelesen werden. Das Paket ist dennoch nutzbar.",
	copyPath: "Pfad kopieren",
	modalTitle: "Diagnosepaket erstellen?",
	modalDescription: "Unifideck sammelt seine Protokolle in einer einzigen ZIP-Datei in Ihrem Downloads-Ordner, damit Sie sie an einen Fehlerbericht anhängen können.",
	modalIncludes: "Enthalten: Plugin- und Spielstart-Protokolle, Synchronisierungs- und Installationsverlauf, Ihre Spieltitel und Installationspfade sowie Angaben zu Ihrem Gerät wie Modell, Betriebssystem und Speicher.",
	modalExcludes: "Niemals enthalten: Ihre Passwörter, Store-Anmeldetoken oder Browser-Cookies.",
	modalConfirm: "Paket erstellen",
	cancel: "Abbrechen"
};
var installButton$d = {
	play: "Spielen",
	resume: "Fortsetzen",
	install: "{{title}} installieren",
	installNative: "Installieren",
	uninstall: "{{title}} deinstallieren",
	cancel: "✖ Abbrechen",
	processing: "Wird verarbeitet...",
	checking: "Überprüfen...",
	size: "{{size}} GB",
	update: "UPDATE",
	updateAvailable: "UPDATE VERFÜGBAR",
	updateAvailableDescription: "Eine neue Version ist installationsbereit."
};
var confirmModals$d = {
	installTitle: "Installation bestätigen",
	installDescription: "Bist du sicher, dass du {{title}} installieren möchtest?",
	uninstallTitle: "Deinstallation bestätigen",
	uninstallDescription: "Bist du sicher, dass du {{title}} deinstallieren möchtest?",
	cancelTitle: "Abbruch bestätigen",
	cancelDescription: "Bist du sicher, dass du den Download von {{title}} abbrechen möchtest?",
	forceSyncTitle: "Synchronisierung erzwingen",
	forceSyncDescription: "Möchtest du alle Coverbilder neu synchronisieren? Dadurch werden manuelle Änderungen an Grafiken überschrieben. Wähle 'Grafiken behalten', um nur fehlende Grafiken herunterzuladen.",
	resyncArtwork: "Grafiken neu synchronisieren",
	keepArtwork: "Grafiken behalten",
	steamRestartTitle: "Synchronisierung abgeschlossen!",
	steamRestartDescription: "Bitte starte Steam neu, um deine neuen Spiele und Grafiken anzuwenden",
	steamRestartDescriptionStore: "Bitte starte Steam neu, um deine neuen {{store}}-Spiele und -Grafiken anzuwenden",
	steamRestartCleanupTitle: "Bereinigung abgeschlossen!",
	steamRestartCleanupDescription: "Bitte starte Steam neu, um gelöschte Spiele und Grafiken vollständig zu entfernen",
	restartNow: "Jetzt neu starten",
	later: "Später",
	ubisoftInstallTitle: "Ubisoft Connect erforderlich",
	ubisoftInstallDescription: "Dadurch wird der Ubisoft Connect Launcher geladen, den du zur Installation von {{title}} benötigst.",
	ubisoftInstallConfirm: "Ubisoft Connect öffnen",
	yes: "Ja",
	no: "Nein",
	amazonUpdateTitle: "Vollständiger erneuter Download erforderlich",
	amazonUpdateDescription: "Amazon-Updates erfordern einen vollständigen erneuten Download von {{title}}. Dies kann einige Zeit dauern. Fortfahren?",
	updateConfirm: "Aktualisieren"
};
var toasts$d = {
	downloadStarted: "Download gestartet. Überprüfe UNIFIDECK > Downloads",
	downloadQueued: "Download in Warteschlange",
	downloadQueuedBody: "{{title}} wurde der Warteschlange hinzugefügt.",
	downloadFailedUnknown: "Unbekannter Fehler.",
	updateQueued: "Update in Warteschlange",
	uninstallDone: "Deinstallation abgeschlossen.",
	ubisoftLauncherOpening: "Ubisoft Connect wird geöffnet",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect wird für {{title}} geöffnet. Installiere das Spiel dort und kehre dann zurück, um zu spielen.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect ist für diese Installation bereits geöffnet.",
	ubisoftLauncherOpenFailed: "Ubisoft Connect konnte nicht geöffnet werden",
	ubisoftLauncherOpenFailedMessage: "Ubisoft Connect konnte für die Installation nicht geöffnet werden.",
	multipartDetected: "Mehrteiliger Download erkannt",
	multipartMessage: "Bitte hab Geduld und warte auf den Abschluss",
	downloadFailed: "Download fehlgeschlagen",
	downloadFailedMessage: "Download konnte nicht gestartet werden.",
	downloadCancelled: "Download abgebrochen",
	downloadCancelledMessage: "Download von {{title}} abgebrochen.",
	cancelFailed: "Abbruch fehlgeschlagen",
	cancelFailedMessage: "Download konnte nicht abgebrochen werden.",
	uninstalling: "Wird deinstalliert...",
	uninstallingMessage: "{{title}} wird deinstalliert...",
	uninstallingMessageProton: "{{title}} und Proton-Dateien werden deinstalliert...",
	uninstallComplete: "Deinstallation abgeschlossen",
	uninstallCompleteMessage: "{{title}} wurde entfernt.",
	uninstallCompleteMessageProton: "{{title}} wurde entfernt (einschließlich Proton-Dateien).",
	installComplete: "Installation abgeschlossen!",
	installCompleteMessage: "{{title}} ist spielbereit.",
	syncComplete: "Synchronisierung abgeschlossen!",
	syncCompleteMessage: "{{count}} Spiele hinzugefügt. STARTE STEAM NEU (vollständig beenden, nicht nur in den Spielmodus zurückkehren), um sie in deiner Bibliothek zu sehen.",
	forceSyncComplete: "Erzwungene Synchronisierung abgeschlossen!",
	forceSyncCompleteMessage: "{{count}} Spiele aktualisiert. STARTE STEAM NEU, um die Änderungen zu sehen.",
	syncCancelled: "Synchronisierung abgebrochen",
	authTimeout: "Authentifizierungs-Zeitüberschreitung",
	authTimeoutMessage: "Authentifizierung für {{store}} abgelaufen. Bitte versuche es erneut.",
	authFailed: "Authentifizierung fehlgeschlagen",
	authFailedMessage: "Authentifizierung konnte nicht gestartet werden",
	authError: "Authentifizierungsfehler",
	authConnected: "{{store}} verbunden!",
	authConnectedMessage: "Erfolgreich mit {{store}} authentifiziert",
	cleanupStarted: "Bereinigung gestartet",
	cleanupStartedMessage: "Verknüpfungen, Grafiken und gespeicherte Logins werden entfernt. Dies kann einen Moment dauern…",
	cleanupSuccessful: "Bereinigung erfolgreich",
	cleanupSuccessfulMessage: "{{games}} Verknüpfungen, {{artwork}} Grafikdateien entfernt, bei {{stores}} Stores abgemeldet, {{files}} Installationsverzeichnisse gelöscht. Cache geleert.",
	cleanupNoop: "Nichts zu bereinigen",
	cleanupNoopMessage: "Unifideck hat keine Verknüpfungen, Grafiken oder gespeicherten Logins zum Entfernen.",
	deleteFailed: "Löschen fehlgeschlagen",
	repairPrefixSuccess: "Präfix repariert",
	repairPrefixSuccessMessage: "Ubisoft Connect-Präfix wurde neu erstellt",
	repairPrefixFailed: "Reparatur fehlgeschlagen",
	repairPrefixFailedMessage: "Präfix konnte nicht repariert werden: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Netzwerkfehler",
		launchRetry: "Start wird wiederholt",
		retryingUmu: "UMU wird in {{seconds}}s erneut versucht (Versuch {{attempt}}/{{max}})...",
		protonUpgrade: "Proton-Upgrade",
		resettingPrefix: "Präfix für {{version}} wird zurückgesetzt...",
		firstTimeSetup: "Ersteinrichtung",
		initializingPrefix: "Wine-Präfix wird initialisiert...",
		setupCompleteTitle: "Einrichtung abgeschlossen",
		prefixInitialized: "Wine-Präfix initialisiert",
		setupFallback: "Einrichtung",
		fallbackInitialization: "Fallback-Initialisierung...",
		launchingGame: "Spiel wird gestartet",
		startingEpicGame: "Epic-Spiel wird gestartet...",
		startingAmazonGame: "Amazon-Spiel wird gestartet...",
		startingGogGame: "GOG-Spiel wird gestartet...",
		startingUbisoftGame: "Ubisoft-Spiel wird gestartet...",
		dependenciesTitle: "Abhängigkeiten werden installiert",
		installingRedistributables: "Spiel-Laufzeitumgebungen werden installiert...",
		dependenciesReady: "Abhängigkeiten bereit",
		redistributablesInstalled: "Laufzeitumgebungen installiert",
		dependenciesStatus: "Abhängigkeiten",
		checkLogs: "Einrichtung abgeschlossen (Protokolle für Details prüfen)",
		installingProton: "Proton wird installiert",
		attemptingInstall: "Versuch, {{version}} zu installieren...",
		epicAuthRequired: "Epic-Login erforderlich",
		epicAuthRequiredMessage: "Bitte melde dich in den Unifideck-Einstellungen bei Epic Games an",
		installingRedist: "Laufzeitumgebungen werden installiert",
		installingRedistMessage: "GOG-Laufzeitumgebungen werden zum ersten Mal installiert...",
		setupFailed: "Einrichtung fehlgeschlagen",
		setupFailedMessage: "Installation fehlgeschlagen, siehe Protokolle für Details",
		prerequisitesTitle: "Voraussetzungen werden installiert",
		installingPrerequisites: "Erforderliche Spielvoraussetzungen werden installiert...",
		prerequisitesReady: "Voraussetzungen bereit",
		prerequisitesInstalled: "Spielvoraussetzungen installiert",
		prerequisitesFailed: "Warnung zu Voraussetzungen",
		prerequisitesFailedMessage: "Voraussetzungen wurden möglicherweise nicht korrekt installiert",
		browserRequired: "Microsoft Edge erforderlich",
		signingIn: "Anmeldung...",
		cloudSyncDownFailed: "Download der Cloud-Spielstände für {{store}} fehlgeschlagen ({{error}}). Es wird mit lokalen Spielständen gespielt.",
		cloudSyncUpFailed: "Upload der Cloud-Spielstände für {{store}} fehlgeschlagen ({{error}}). Dein Fortschritt ist nur lokal gespeichert.",
		errorCircuitBreakerOpen: "{{game_key}} konnte in letzter Zeit {{count}} Mal nicht gestartet werden. Automatische Startversuche pausiert.",
		launcherError: "{{game_key}} konnte nicht gestartet werden ({{error_code}}). Siehe Protokolle für Details.",
		downloadingProton: "Proton wird heruntergeladen – das kann einige Minuten dauern...",
		downloadingRuntime: "Steam Runtime wird heruntergeladen – der erste Start kann einige Minuten dauern...",
		installingUbisoft: "Installation über Ubisoft Connect",
		installingUbisoftMessage: "Ubisoft Connect wird geöffnet – starte die Installation im Launcher-Fenster.",
		protonReadyBody: "{{version}} ist einsatzbereit",
		protonReadyTitle: "Proton bereit",
		protonSwitchedTo: "{{version}} wird jetzt verwendet",
		signingInUbisoft: "Anmeldung bei Ubisoft Connect",
		signingInUbisoftMessage: "Ubisoft Connect wird geöffnet – melde dich dort an und kehre dann zurück.",
		ubisoftPrefixNotReady: "Ubisoft Connect ist nicht bereit",
		ubisoftPrefixNotReadyMessage: "Das Ubisoft-Connect-Prefix für dieses Spiel konnte nicht vorbereitet werden. Versuche die Installation erneut; wenn es weiterhin fehlschlägt, führe eine Bibliotheks-Synchronisierung erneut aus.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect ist nicht bereit",
		ubisoftAuthPrefixNotReadyMessage: "Ubisoft Connect konnte nicht für die Anmeldung vorbereitet werden. Versuche es in einem Moment erneut.",
		ubisoftLaunchIdMissing: "Ubisoft Connect wird geöffnet",
		ubisoftLaunchIdMissingMessage: "Die Start-ID für dieses Spiel konnte nicht ermittelt werden – Ubisoft Connect wird geöffnet, damit du es aus deiner Bibliothek starten kannst. Führe eine Bibliotheks-Synchronisierung erneut aus, um den Direktstart zu reparieren."
	},
	updateComplete: "Update abgeschlossen!",
	updateCompleteMessage: "{{title}} wurde aktualisiert.",
	updateCancelled: "Update abgebrochen",
	updateCancelledMessage: "Update für {{title}} wurde abgebrochen.",
	updateFailed: "Update fehlgeschlagen",
	updateFailedMessage: "Update konnte nicht gestartet werden.",
	storeError: "Verbindung mit {{store}} fehlgeschlagen ({{errType}})",
	captureLogsStarted: "Protokolle werden gesammelt...",
	captureLogsStartedMessage: "Diagnosepaket wird in Ihrem Downloads-Ordner erstellt. Das dauert einige Sekunden.",
	captureLogsDone: "Protokolle erfasst",
	captureLogsDoneMessage: "Gespeichert unter {{path}} ({{size}}, {{files}} Dateien).",
	captureLogsPartial: "Protokolle erfasst (unvollständig)",
	captureLogsPartialMessage: "Gespeichert unter {{path}} ({{size}}, {{files}} Dateien). {{failures}} Datei(en) konnten nicht gelesen werden.",
	captureLogsFailed: "Protokolle konnten nicht erfasst werden",
	captureLogsBusy: "Eine Protokollerfassung läuft bereits",
	captureLogsCopied: "Pfad kopiert",
	captureLogsCopyFailed: "Pfad konnte nicht kopiert werden",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Speicherplatz freigeben",
		signInToStore: "Anmelden",
		retrySync: "Wiederholen",
		retryLibrarySync: "Bibliothek aktualisieren",
		openSaveFolder: "Speicherordner anzeigen",
		showLogs: "Protokolle anzeigen"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Bibliothek von {{store}} konnte nicht aktualisiert werden ({{error}}). Deine vorhandene Bibliothek bleibt sichtbar."
	},
	cloudPullStarted: "Cloud-Spielstand wird heruntergeladen…",
	cloudPushStarted: "Lokaler Spielstand wird hochgeladen…",
	cloudSavePathFailed: "Speicherort konnte nicht festgelegt werden",
	cloudSavePathSet: "Speicherort aktualisiert",
	cloudPullDone: "Cloud-Spielstand heruntergeladen",
	cloudPullFailed: "Cloud-Spielstand-Download fehlgeschlagen",
	cloudPushDone: "Lokaler Spielstand hochgeladen",
	cloudPushFailed: "Lokaler Spielstand-Upload fehlgeschlagen"
};
var uninstallModal$d = {
	title: "Deinstallation bestätigen",
	description: "Bist du sicher, dass du {{title}} deinstallieren möchtest?",
	deleteProtonLabel: "Auch Proton-Dateien löschen",
	deleteProtonDescription: "Entfernt Kompatibilitätsdaten und Benutzerdateien",
	warningTitle: "Warnung:",
	warningBody: "Dadurch werden alle im Wine-Präfix gespeicherten Spielstände dauerhaft gelöscht. Stelle sicher, dass sie gesichert sind.",
	cancel: "Abbrechen",
	uninstall: "Deinstallieren"
};
var languageSettings$d = {
	title: "SPRACHE",
	label: "Anzeigesprache",
	description: "Sprache für Benutzeroberfläche und Spieledownloads (wo unterstützt)",
	autoDetect: "Automatisch (System)"
};
var errors$d = {
	legendaryNotFound: "Epic Games CLI (Legendary) nicht gefunden",
	gogdlNotFound: "GOG CLI (gogdl) nicht gefunden",
	nileNotFound: "Amazon Games CLI (Nile) nicht gefunden",
	authUrlFailed: "Authentifizierungs-URL konnte nicht abgerufen werden",
	authFailed: "Authentifizierung fehlgeschlagen",
	notAuthenticated: "Nicht authentifiziert. Bitte melde dich zuerst an.",
	syncInProgress: "Eine Synchronisierung wird bereits ausgeführt",
	syncCancelled: "Synchronisierung vom Benutzer abgebrochen",
	gameNotFound: "Spiel nicht gefunden",
	gameNotInstalled: "Spiel ist nicht installiert",
	installFailed: "Installation fehlgeschlagen - Details siehe Protokolle",
	installNoDirectory: "Installation abgeschlossen, aber Spielverzeichnis konnte nicht gefunden werden",
	uninstallFailed: "Deinstallation fehlgeschlagen",
	lockConflict: "Sperrkonflikt - bitte versuche es erneut",
	unknownStore: "Unbekannter Store",
	shortcutWriteFailed: "Verknüpfungen konnten nicht geschrieben werden",
	backgroundSyncDisabled: "Hintergrund-Synchronisierung deaktiviert",
	steamGridDbUnavailable: "SteamGridDB-Bibliothek nicht verfügbar",
	deleteSyncInProgress: "Löschen nicht möglich, während eine Synchronisierung läuft",
	alreadyInQueue: "Bereits in Warteschlange",
	download: {
		diskSpace: "Nicht genügend freier Speicherplatz vorhanden",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Netzwerkfehler — bitte überprüfe deine Verbindung",
		authExpired: "Anmeldung abgelaufen — bitte melde dich erneut an",
		toolMissing: "Erforderliches Download-Werkzeug nicht gefunden",
		processFailed: "Download fehlgeschlagen — bitte versuche es erneut",
		directoryNotFound: "Installierte Dateien konnten nicht gefunden werden",
		lockConflict: "Ein anderer Vorgang wird ausgeführt — versuche es erneut",
		ubisoftClient: "Ubisoft Connect-Client-Fehler — versuche das Präfix zu reparieren",
		ubisoftClientNotRunning: "Ubisoft Connect-Client antwortet nicht",
		ubisoftBootstrap: "Ubisoft Connect-Präfix konnte nicht eingerichtet werden",
		ubisoftUmuMissing: "umu-run nicht gefunden — für Ubisoft-Spiele erforderlich",
		generic: "Installation fehlgeschlagen — Details siehe Protokolle"
	},
	unknown: "Unbekannter Fehler",
	steamAppsUnavailable: "SteamClient.Apps API nicht verfügbar"
};
var sync$d = {
	start: "Bibliotheken synchronisieren",
	force: "Synchronisierung erzwingen",
	syncing: "Synchronisieren…",
	cancel: "Synchronisierung abbrechen",
	cancelling: "Wird abgebrochen…",
	preparing: "Vorbereitung…",
	downloadingArtwork: "Coverbild für {{game}} wird heruntergeladen...",
	fetchingGameLists: "Spielelisten werden abgerufen...",
	fetchingStore: "{{store}} wird abgerufen ({{current}}/{{total}})...",
	cancelledByUser: "Synchronisierung vom Benutzer abgebrochen",
	checkingInstalledGames: "Installierte Spiele werden überprüft...",
	sgdbLookup: "{{count}} Spiele werden auf SteamGridDB gesucht...",
	extractingSteamMetadata: "Steam-Metadaten werden extrahiert...",
	fetchingEnhancedMetadata: "Erweiterte Metadaten werden abgerufen...",
	lookingUpUnifiDB: "unifiDB-Metadaten werden abgerufen...",
	fetchingMetacriticData: "Metacritic-Metadaten werden abgerufen...",
	fetchingCompatData: "Kompatibilitätsdaten werden abgerufen...",
	checkingExistingArtwork: "Vorhandene Grafiken werden überprüft...",
	cleaningOrphanedArtwork: "Verwaiste Grafiken werden bereinigt...",
	retryingMissingArtwork: "Erneuter Versuch für {{synced}} / {{total}} Spiele mit fehlenden Grafiken...",
	savingShortcuts: "Verknüpfungen werden gespeichert...",
	completed: "Synchronisierung abgeschlossen!",
	extractingMetadata: "Metadaten für {{game}} werden extrahiert...",
	cooldown: "Warte {{secs}}s",
	artworkPhase: "Grafik {{done}} / {{total}}"
};
var force_sync$d = {
	migratingOldInstallations: "Erzwungener Sync: Alte Installationen werden migriert...",
	fetchingGameLists: "Erzwungener Sync: Spielelisten werden abgerufen...",
	checkingInstalledGames: "Erzwungener Sync: Installierte Spiele werden überprüft...",
	rewritingShortcuts: "Erzwungener Sync: Verknüpfungen werden neu geschrieben...",
	cancelledShortcutsSaved: "Erzwungener Sync abgebrochen - Verknüpfungen gespeichert",
	writingShortcuts: "Erzwungener Sync: Verknüpfungen werden geschrieben...",
	clearingProton: "Erzwungener Sync: Proton-Kompatibilität wird bereinigt...",
	completed: "Erzwungener Sync abgeschlossen! {{updated}} Spiele aktualisiert, {{artwork}} Coverbilder abgerufen."
};
var artwork$d = {
	checking: "Grafiken werden überprüft...",
	queueRefresh: "Alle Spiele werden zur Grafik-Aktualisierung eingereiht...",
	downloadProgress: "Grafiken heruntergeladen: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$d = {
	greatOnDeck: "Hervorragend auf Deck",
	allGames: "Alle Spiele",
	installed: "Installiert",
	nonSteam: "Fachfremde",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$d = {
	error: "Unifideck-Fehler",
	errorLoadingView: "Einheitliche Bibliotheksansicht konnte nicht geladen werden",
	checkConsole: "Weitere Details findest du in der Browser-Konsole",
	errorLoadingGames: "Fehler beim Laden der Spiele",
	tryReloading: "Versuche, das Plugin neu zu laden oder überprüfe die Konsole auf Details",
	storeLabel: "Store:",
	allStores: "Alle Stores",
	searchPlaceholder: "Spiele durchsuchen...",
	library: "Bibliothek"
};
var gameInfoPanel$d = {
	loading: "Spielinformationen werden geladen...",
	noMetadata: "Keine Metadaten für dieses Spiel verfügbar.",
	noTestResults: "Keine detaillierten Testergebnisse verfügbar.",
	buttons: {
		details: "Details",
		synopsis: "Zusammenfassung",
		storePage: "Shop-Seite",
		dlc: "DLC",
		communityHub: "Community-Hub",
		pointsShop: "Punkteshop",
		discussions: "Diskussionen",
		guides: "Guides",
		support: "Support",
		install: "Installieren",
		uninstall: "Deinstallieren",
		cancel: "Abbrechen",
		achievements: "Erfolge"
	},
	labels: {
		store: "Store:",
		name: "Name:",
		size: "Größe:",
		developer: "Entwickler:",
		publisher: "Publisher:",
		released: "Veröffentlicht:",
		metacritic: "Metacritic:",
		status: "Status:",
		spaceRequired: "Benötigter Speicher",
		lastPlayed: "Zuletzt gespielt",
		playTime: "Spielzeit",
		never: "Nie"
	},
	compatibility: {
		modalTitle: "Steam Deck Kompatibilität",
		viewOnProtonDb: "Auf ProtonDB ansehen",
		close: "Schließen",
		verified: "VERIFIZIERT",
		playable: "SPIELBAR",
		unsupported: "NICHT UNTERSTÜTZT",
		unknown: "UNBEKANNT"
	},
	failedToLoadMetadata: "Metadaten konnten nicht geladen werden",
	lastSession: "Letzte Sitzung: {{names}} freigeschaltet"
};
var gameDetailsSettings$d = {
	title: "SPIELDETAIL-ANSICHT",
	simple: "Einfach",
	detailed: "Detailliert"
};
var gogLanguageModal$d = {
	title: "Sprache auswählen",
	description: "Wähle die Sprache, in der {{title}} heruntergeladen werden soll",
	label: "Sprache",
	install: "Installieren",
	cancel: "Abbrechen"
};
var accountSwitch$d = {
	title: "Konto-Wechsel erkannt",
	description: "Ein anderes Steam-Konto ist jetzt angemeldet. Was möchten Sie mit Ihren Unifideck-Daten tun?",
	migrate: "Unifideck-Daten migrieren",
	migrateDescription: "Spiele & Grafiken vom vorherigen Konto kopieren",
	freshStart: "Neuanfang",
	freshStartDescription: "Store-Logins löschen, um andere Epic/GOG/Amazon/Ubisoft-Konten zu verbinden",
	skip: "Überspringen",
	toastMigrateTitle: "Migration abgeschlossen",
	toastMigrateBody: "{{shortcuts}} Verknüpfungen und {{artwork}} Grafikdateien migriert",
	toastClearTitle: "Authentifizierungstoken gelöscht",
	toastClearBody: "Store-Logins wurden gelöscht. Sie können sich jetzt mit anderen Konten anmelden."
};
var ubisoftAuth$d = {
	title: "Ubisoft Connect Anmeldung",
	description: "Melden Sie sich über Ubisoft Connect an, um Ihre Bibliothek zu verknüpfen.",
	signInViaUpc: "Über Ubisoft Connect anmelden",
	upcDescription: "Ubisoft Connect wird geöffnet, damit Sie sich anmelden können. Sobald Sie angemeldet sind, wird Ihre Sitzung automatisch erfasst.",
	launchingUpc: "Ubisoft Connect wird gestartet...",
	waitingForLogin: "Warten auf Anmeldung...",
	retryLaunch: "Wiederholen",
	cancel: "Abbrechen",
	launchFailed: "Ubisoft Connect konnte nicht gestartet werden. Bitte versuchen Sie es erneut.",
	signingIn: "Anmeldung läuft...",
	emailLabel: "E-Mail",
	passwordLabel: "Passwort",
	signIn: "Anmelden",
	twoFactorTitle: "Zwei-Faktor-Authentifizierung",
	twoFactorDescription: "Geben Sie den Verifizierungscode aus Ihrer Authentifikator-App ein.",
	twoFactorLabel: "Verifizierungscode",
	verify: "Verifizieren",
	verifying: "Verifizieren...",
	errorEmptyFields: "Bitte geben Sie E-Mail und Passwort ein",
	errorEmpty2FA: "Bitte geben Sie den Verifizierungscode ein",
	errorLoginFailed: "Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldedaten.",
	errorInvalid2FA: "Ungültiger Verifizierungscode. Bitte versuchen Sie es erneut.",
	showPassword: "Passwort anzeigen",
	hidePassword: "Passwort ausblenden",
	doubleLoginWarning: "Sie müssen sich für Ubisoft zweimal anmelden."
};
var relativeDate$d = {
	today: "Heute",
	yesterday: "Gestern"
};
var common$d = {
	thisGame: "dieses Spiel",
	game: "Spiel",
	unknown: "Unbekannt",
	ok: "OK",
	cancel: "Abbrechen",
	working: "Arbeiten…",
	loading: "Laden…",
	free: "kostenlos",
	close: "Schließen"
};
var info$d = {
	expandDetails: "Details anzeigen",
	collapseDetails: "Details ausblenden"
};
var microsoft$d = {
	notCompatible: "Dieses Spielformat ist nicht mit SteamOS kompatibel",
	playOnCloud: "In der Cloud spielen",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Melden Sie sich mit Ihrem Microsoft- / Xbox-Konto an",
	accountConnected: "Microsoft-Konto verbunden",
	loggedOut: "Abgemeldet vom Microsoft Store",
	xcloudNotInstalled: "xCloud-Spiele sind lokal nicht installiert",
	chromiumRequired: "Microsoft Edge erforderlich",
	chromiumRequiredMessage: "Die Anmeldung erfordert Microsoft Edge. Möchten Sie es automatisch installieren? (Dies muss nur einmal durchgeführt werden.)",
	chromiumInstallButton: "Microsoft Edge installieren",
	chromiumInstalling: "Microsoft Edge wird installiert...",
	chromiumInstalled: "Microsoft Edge installiert!",
	chromiumAlreadyInstalled: "Ein kompatibler Browser ist bereits installiert",
	chromiumInstallFailed: "Installation von Microsoft Edge fehlgeschlagen. Bitte manuell installieren: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Installation von Microsoft Edge zeitlich überschritten. Bitte überprüfen Sie Ihre Internetverbindung.",
	browserAlreadyInstalled: "Ein kompatibler Browser ist bereits installiert",
	browserInstalled: "Microsoft Edge erfolgreich installiert!",
	browserInstallFailed: "Installation von Microsoft Edge fehlgeschlagen. Bitte manuell installieren: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak ist auf diesem System nicht verfügbar",
	signInRequired: "Zum Spielen anmelden",
	syncSkippedNoSubscription: "Kein aktives Xbox Game Pass-Abonnement erkannt. Ihre xCloud-Bibliothek wurde nicht synchronisiert.",
	syncSkippedTierUnknown: "Ihr Xbox Game Pass-Abonnement wurde erkannt, aber der Tarif konnte nicht ermittelt werden. Synchronisierung übersprungen, um nicht verfügbare Spiele nicht aufzulisten.",
	subscriptionCheckFailed: "Ihr Xbox Game Pass-Abonnement konnte nicht verifiziert werden. xCloud-Synchronisierung übersprungen. Versuchen Sie es erneut, wenn sich Ihre Verbindung stabilisiert hat.",
	subscriptionDetected: "Aktives Xbox Game Pass {{tier}}-Abonnement."
};
var playButton$d = {
	play: "Spielen",
	install: "Installieren",
	cancel: "Abbrechen",
	retry: "Wiederholen",
	downloadFailed: "Download fehlgeschlagen",
	installing: "Installieren",
	controllerConfig: "Controller-Konfiguration",
	appSettings: "App-Einstellungen"
};
var play$d = {
	play: "Spielen",
	resume: "Fortsetzen",
	stop: "Stoppen",
	cancel: "Abbrechen",
	cancelling: "Wird abgebrochen…",
	update: "Aktualisieren",
	uninstall: "Deinstallieren",
	queued: "In Warteschlange",
	downloading: "Wird heruntergeladen",
	extracting: "Wird entpackt",
	verifying: "Wird überprüft",
	finalizing: "Installation wird abgeschlossen",
	cancelConfirmTitle: "Download abbrechen?",
	cancelConfirmBody: "Sind Sie sicher, dass Sie den Download für {{title}} abbrechen möchten? Unvollständige Dateien werden verworfen.",
	cancelConfirmConfirm: "Download abbrechen",
	cancelConfirmCancel: "Download fortsetzen",
	playOnCloud: "In der Cloud spielen",
	cloudSave: {
		autoPullOn: "Cloud-Spielstände werden beim Start automatisch heruntergeladen.",
		close: "Schließen",
		download: "Cloud-Spielstand herunterladen",
		filesCount: "{{count}} Dateien",
		label: "Cloud-Spielstände",
		menuTitle: "Cloud-Spielstände — {{game}}",
		noCloudData: "Unbekannt",
		noCloudSupport: "Keine Cloud-Speicher-Unterstützung",
		setLocation: "Speicherort festlegen",
		setLocationHint: "Wähle den Ordner aus, in dem dieses Spiel seine Spielstände speichert.",
		setLocationTitle: "Speicherordner dieses Spiels wählen",
		statusCloudAvailable: "Cloud-Spielstand verfügbar",
		statusNoSaves: "Keine lokalen Spielstände",
		statusSyncing: "Cloud-Spielstände werden synchronisiert…",
		statusUnresolved: "Speicherort nicht gefunden — Synchronisation funktioniert für dieses Spiel eventuell nicht.",
		unresolvedHint: "Starte das Spiel einmal, damit wir den Speicherordner erkennen können, oder lege den Ort manuell fest.",
		upload: "Lokalen Spielstand hochladen"
	},
	exe: {
		browse: "Durchsuchen…",
		changeFailed: "Ausführbare Datei konnte nicht geändert werden",
		changed: "Ausführbare Datei geändert",
		defaultTag: "(Standard)",
		menuItem: "Ausführbare Datei ändern…",
		reset: "Auf Standard-Datei zurücksetzen",
		resetButton: "Auf Standard zurücksetzen",
		subtitle: "Wählen Sie aus, welche Datei dieses Spiel startet (z. B. um einen Launcher zu überspringen und das Spiel direkt auszuführen). Das Arbeitsverzeichnis und Cloud-Spielstände sind nicht betroffen.",
		title: "Ausführbare Datei ändern — {{game}}",
		unavailable: "Der Ordner dieses Spiels konnte nicht gelesen werden."
	}
};
var playMeta$d = {
	spaceRequired: "Benötigter Speicherplatz",
	installedSize: "Installierte Größe",
	lastPlayed: "Zuletzt gespielt",
	neverPlayed: "Nie gespielt",
	hoursShort: "Std.",
	minutesShort: "Min.",
	played: "Gespielt"
};
var downloads$d = {
	title: "Downloads",
	empty: "Keine Downloads. Installieren Sie Spiele aus Ihrer Bibliothek, um sie hier zu sehen.",
	current: "Wird jetzt heruntergeladen",
	queued: "In Warteschlange",
	finished: "Kürzlich fertiggestellt",
	cancel: "Abbrechen",
	removeFromQueue: "Entfernen",
	play: "Spielen",
	outcome: {
		queued: "In Warteschlange",
		downloading: "Wird heruntergeladen",
		completed: "Abgeschlossen",
		cancelled: "Abgebrochen",
		error: "Fehlgeschlagen"
	}
};
var navigation$d = {
	loading: "Laden...",
	storesTitle: "Stores",
	syncTitle: "Bibliotheks-Sync",
	languageTitle: "Sprache"
};
var security$d = {
	panelTitle: "Sicherheit",
	counters: {
		label: "Zähler",
		empty: "Seit dem Start wurden keine Ereignisse beobachtet"
	},
	log: {
		label: "Audit-Protokoll",
		entryCount: "{{count}} Eintrag",
		entryCount_other: "{{count}} Einträge",
		emptyFiltered: "Keine Ereignisse entsprechen dem Filter"
	},
	filter: {
		label: "Filter",
		all: "Alle Ereignisse"
	},
	timeline: {
		label: "24-Stunden-Zeitachse ({{count}} Ereignis)",
		label_other: "24-Stunden-Zeitachse ({{count}} Ereignisse)",
		axisStart: "-24 Std.",
		axisEnd: "Jetzt"
	},
	relativeTime: {
		seconds: "vor {{count}}s",
		minutes: "vor {{count}} Min.",
		hours: "vor {{count}} Std.",
		days: "vor {{count}} Tagen"
	},
	bruteforce: {
		warningTitle: "Warnung: Brute-Force-Erkennung (Warnstufe)",
		escalationTitle: "ALARM: Brute-Force-Erkennung (Eskalationsstufe)",
		detail: "{{failures}} Entschlüsselungsfehler in den letzten {{window}}s. Schwellenwerte: {{warning}} (Warnung) / {{escalation}} (Eskalation).",
		resetButton: "Erkennen und Zurücksetzen des Detektors"
	},
	actions: {
		clearLog: "Protokoll und Zähler löschen"
	},
	events: {
		tokenEncrypted: "Token verschlüsselt",
		tokenDecrypted: "Token entschlüsselt",
		decryptFailed: "Entschlüsselung fehlgeschlagen",
		tokenFileMigrated: "Tokendatei migriert",
		legacyPlaintextDetected: "Klassischer Klartext erkannt",
		authFlowStarted: "Authentifizierung gestartet",
		authFlowCompleted: "Authentifizierung abgeschlossen",
		authFlowFailed: "Authentifizierung fehlgeschlagen",
		permissionsCheck: "Berechtigungen geprüft",
		permissionsRepaired: "Berechtigungen repariert",
		bruteforceSuspected: "Brute-Force vermutet",
		deviceResetDetected: "Geräte-Reset erkannt",
		fingerprintInitialized: "Fingerabdruck initialisiert",
		externalAuthCheckFailed: "Externe Authentifizierungsprüfung fehlgeschlagen",
		configValidationCompleted: "Konfigurationsvalidierung abgeschlossen",
		configValidationFailed: "Konfigurationsvalidierung fehlgeschlagen"
	},
	configValidation: {
		degradedTitle: "Konfiguration ungültig - eingeschränkter Modus aktiv",
		detail: "{{count}} Validierungsfehler beim Booten erkannt",
		detail_other: "{{count}} Validierungsfehler beim Booten erkannt",
		sourceDefaults: "Standardwerte",
		sourceUser: "Benutzer-Überschreibungen",
		sourceMerged: "zusammengeführt",
		overflow: "+ {{count}} weiterer Fehler",
		overflow_other: "+ {{count}} weitere Fehler"
	}
};
var cloudSyncBehavior$d = {
	_comment: "Einstellungsfenster, mit dem der Benutzer pro Store auswählen kann, wie Synchronisationsfehler bei Cloud-Spielständen gemeldet werden sollen. Die fünf store_label-Einträge entsprechen den von CloudSyncBehaviorPanel.tsx gerenderten Zeilen. Microsoft wurde absichtlich weggelassen – xCloud-Streaming hat keinen lokalen Spielstand zum Synchronisieren.",
	title: "Verhalten bei Cloud-Spielstand-Synchronisation",
	description: "Wählen Sie aus, wie Unifideck reagieren soll, wenn ein Download oder Upload des Cloud-Spielstands beim Spielstart fehlschlägt.",
	loadError: "Aktuelle Einstellungen konnten nicht geladen werden",
	saveError: "Ihre Änderung konnte nicht gespeichert werden – bitte versuchen Sie es erneut",
	store_label: {
		"default": "Standard (alle Stores)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Stumm (nur Protokoll)",
		toast: "Benachrichtigen (Toast)"
	}
};
var cloudSync$d = {
	_comment: "Vom Launcher ausgegebene Toast-Meldungen, wenn eine Cloud-Synchronisierung fehlschlägt. Der Fehler-Platzhalter wird über cloudSync.error.<code> über das Feld error_i18n_key der Backend-Payload aufgelöst.",
	error: {
		network_unreachable: "Netzwerk nicht erreichbar",
		auth_expired: "Authentifizierung abgelaufen",
		forbidden: "Zugriff verweigert",
		quota_exceeded: "Cloud-Quota überschritten",
		server_error: "Store-Server nicht verfügbar",
		timed_out: "Anfrage abgelaufen",
		disk_full: "Nicht genügend Speicherplatz",
		disk_space_low: "Übersprungen — wenig Speicherplatz (Speicherplatz freigeben und erneut versuchen)",
		permission_denied: "Berechtigung verweigert",
		cancelled: "Abgebrochen",
		unknown: "Unerwarteter Fehler"
	},
	unavailableNativeDep: "Cloud-Spielstände sind auf diesem System deaktiviert — der Python-Installation ({{python}}) fehlt eine erforderliche Komponente. Ihre lokalen Spielstände sind nicht betroffen. Erstellen/installieren Sie das Plugin nach einem Systemupdate neu, um die Synchronisierung wiederherzustellen."
};
var launchLogs$d = {
	_comment: "Zeichenfolgen für LaunchLogsModal. Das Modal wird über die Toast-Aktion unifideck://show-logs/<launch_id> geöffnet, die an Launcher-Fehler-Toasts (Circuit Breaker + LauncherError) angehängt ist. Liest archivierte Protokolle pro Start aus ~/.local/share/unifideck/launches/<launch_id>.log und rendert sie mit stufenbasierter Farbcodierung.",
	title: "Startprotokolle — {{launchId}}",
	close: "Schließen",
	copyAll: "Alle kopieren",
	"export": "In Datei exportieren",
	copied: "In die Zwischenablage kopiert",
	copyFailed: "Kopieren fehlgeschlagen",
	exporting: "Exportieren…",
	exportedTo: "Exportiert nach {{path}}",
	exportFailed: "Export fehlgeschlagen: {{error}}",
	loading: "Protokolle werden geladen…",
	notAvailable: "Protokolle für diesen Start sind nicht mehr verfügbar (die Aufbewahrungsfrist von 7 Tagen ist abgelaufen).",
	error: "Protokolle konnten nicht gelesen werden: {{error}}",
	fileInfo: "{{path}} — Zeige {{shown}} von {{total}} Zeilen",
	filterLabel: "Anzeigen",
	filterInfo: "Info",
	filterWarning: "Warnung",
	filterError: "Fehler",
	nextError: "Nächster Fehler ({{current}}/{{total}})",
	noErrors: "Keine Fehler",
	allFiltered: "Alle Zeilen durch den aktuellen Filter ausgeblendet. Aktivieren Sie mindestens eine Ebene, um Protokolleinträge zu sehen."
};
var settings$d = {
	_comment: "Aktionen des UnifideckSettingsPanel, die nicht durch spezifischere Komponenten abgedeckt sind (LanguageSelector, CloudSyncBehaviorPanel). refreshAll ist eine Schaltfläche zum Starten von sync_all() für alle registrierten Stores; die Bibliotheksansicht zeigt den Synchronisierungsfortschritt über ein eigenes Abonnement für SYNC_PROGRESS-Ereignisse an.",
	storeConnections: "Store-Verbindungen",
	librarySync: "Bibliotheks-Sync",
	language: "Sprache",
	languageAuto: "Automatisch erkennen",
	refreshAll: {
		sectionTitle: "Bibliotheksaktionen",
		button: "Alle Bibliotheken aktualisieren",
		buttonBusy: "Starten…",
		startedTitle: "Bibliotheksaktualisierung gestartet",
		startedBody: "Alle Stores werden im Hintergrund synchronisiert.",
		startedSingle: "{{store}}-Bibliothek wird im Hintergrund aktualisiert.",
		failedTitle: "Bibliotheksaktualisierung konnte nicht gestartet werden"
	}
};
var saveFolder$d = {
	_comment: "Zeichenfolgen für SaveFolderModal. Das Modal wird über die Toast-Aktion unifideck://open-save-folder/<store>/<game_id> geöffnet, die normalerweise bei Cloud-Synchronisierungsfehlern angezeigt wird, wenn der Benutzer überprüfen möchte, was tatsächlich lokal gespeichert ist. count und size werden interpoliert; truncated wird nur angezeigt, wenn das Ergebnis begrenzt wurde.",
	title: "Speicherordner — {{store}} / {{gameId}}",
	close: "Schließen",
	copyPath: "Pfad kopieren",
	copyFileList: "Dateiliste kopieren",
	depthLabel: "Tiefe",
	filterLabel: "Filter",
	loading: "Ordnerinhalt wird geladen…",
	noSavesYet: "Für dieses Spiel wurden lokal noch keine Spielstände gefunden. Der Ordner wird bei der nächsten erfolgreichen Cloud-Synchronisierung erstellt.",
	aggregate: "{{count}} Dateien, insgesamt {{size}}",
	truncated: "{{count}} weitere Dateien ({{size}}) nicht angezeigt – engen Sie Ihren Filter ein, um sie zu sehen.",
	error: "Speicherordner konnte nicht gelesen werden: {{error}}"
};
var library$d = {
	_comment: "Hilfen pro Spiel in der Bibliotheksansicht, einschließlich des Circuit-Breaker-Badges und der Schaltflächen zum erzwungenen Starten / Zurücksetzen bei wiederholten Fehlstarts.",
	circuitBreaker: {
		_comment: "Oberfläche für den Startfehler-Circuit-Breaker. Das Badge wird nur angezeigt, wenn die Anzahl der Fehler innerhalb des konfigurierten Fensters den Schwellenwert circuit_breaker.failures_threshold erreicht (Standard 3 in 600s). Erzwungener Start aktiviert eine einmalige Umgehung; Zurücksetzen löscht den Fehlerverlauf vollständig.",
		badgeRecent: "{{count}} kürzlicher Startfehler",
		badgeRecent_other: "{{count}} kürzliche Startfehler",
		badgeTooltip: "{{count}} fehlgeschlagene Starts im letzten Zeitfenster – der Circuit-Breaker hat automatische Startversuche pausiert",
		forceLaunchButton: "Start erzwingen",
		resetButton: "Fehler zurücksetzen",
		failureKind: {
			fastBoot: "Absturz beim Schnellstart",
			launcherError: "Fehler bei der Starteinrichtung"
		}
	}
};
var pickStorage$d = {
	title: "Installationsort",
	customOption: "Anderen Ort auswählen...",
	customLabel: "Benutzerdefinierter Ort",
	change: "Ändern...",
	gameSize: "Installationsgröße"
};
var restart$d = {
	title: "Synchronisierung abgeschlossen!",
	body: "Bitte starte Steam neu, um deine neuen Spiele und Grafiken anzuwenden",
	confirm: "Jetzt neu starten",
	later: "Später"
};
var updater$d = {
	checkButton: "Nach Updates suchen",
	checkCompleteMessage: "Informationen zur neuesten Version wurden erfolgreich abgerufen.",
	checkCompleteTitle: "Update-Prüfung abgeschlossen",
	checkFailedTitle: "Prüfung fehlgeschlagen",
	checkingButton: "Prüfung...",
	currentTitle: "Aktuell",
	downgradeButton: "Downgrade auf v{{version}}",
	installFailedTitle: "Installation fehlgeschlagen",
	installTimeoutMessage: "Keine Antwort von Decky Loader — die Installation ist möglicherweise hängen geblieben oder fehlgeschlagen. Bitte versuche es erneut.",
	installedLabel: "installiert",
	installingButton: "Wird installiert...",
	installingTitle: "Plugin wird installiert",
	latestLabel: "neueste",
	modalTitle: "UNIFIDECK v{{version}} — Release Notes",
	noBackend: "Decky-Backend ist nicht verfügbar.",
	noReleaseNotes: "Keine Release Notes verfügbar.",
	reinstallButton: "Reinstall v{{version}}",
	refreshFailedMessage: "Die Informationen zur neuesten Version konnten nicht überprüft werden. Prüfe deine Verbindung und versuche es erneut.",
	releaseGoneMessage: "Diese Version ist nicht mehr verfügbar. Bitte suche nach Updates und wähle erneut aus.",
	releaseNotesButton: "Release Notes",
	stageDownload: "Herunterladen…",
	stageFinish: "Fertigstellen…",
	stageInstall: "Installieren…",
	stageOpen: "Paket lesen…",
	stageParse: "Überprüfen…",
	stageRemove: "Alte Version wird entfernt…",
	stageStart: "Starten…",
	stageWorking: "Installieren…",
	titleLoading: "Version wird geprüft",
	typeDowngrade: "Downgrading auf",
	typeReinstall: "Neuinstallation",
	typeUpdate: "Update auf",
	updateButton: "Update auf v{{version}}"
};
var achievements$d = {
	unlockedTitle: "Erfolg freigeschaltet",
	unlockedBody: "{{name}}"
};
var achievementsModal$d = {
	title: "Erfolge",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Aktualisieren",
	close: "Schließen",
	loading: "Erfolge werden geladen…",
	empty: "Dieses Spiel hat keine Erfolge.",
	hidden: "Verborgener Erfolg",
	unlockedOn: "Freigeschaltet am {{date}}",
	error: "Erfolge konnten nicht geladen werden.",
	errorOffline: "Stelle eine Internetverbindung her, um Erfolge anzuzeigen.",
	errorAuth: "Melde dich im Store dieses Spiels an, um Erfolge anzuzeigen.",
	errorUnavailable: "Für dieses Spiel sind keine Erfolge verfügbar."
};
var collectionSettings$d = {
	title: "STEAM-SAMMLUNGEN",
	enabled: "Sammlungen aktiviert",
	disabled: "Sammlungen deaktiviert"
};
var deDE = {
	_comment: _comment$d,
	tabs: tabs$d,
	auth: auth$d,
	storage: storage$d,
	storageSettings: storageSettings$d,
	gameGrid: gameGrid$d,
	downloadsTab: downloadsTab$d,
	cloudSave: cloudSave$d,
	storeConnections: storeConnections$d,
	librarySync: librarySync$d,
	cleanup: cleanup$d,
	captureLogs: captureLogs$d,
	installButton: installButton$d,
	confirmModals: confirmModals$d,
	toasts: toasts$d,
	uninstallModal: uninstallModal$d,
	languageSettings: languageSettings$d,
	errors: errors$d,
	sync: sync$d,
	force_sync: force_sync$d,
	artwork: artwork$d,
	deckTabs: deckTabs$d,
	unifiedLibrary: unifiedLibrary$d,
	gameInfoPanel: gameInfoPanel$d,
	gameDetailsSettings: gameDetailsSettings$d,
	gogLanguageModal: gogLanguageModal$d,
	accountSwitch: accountSwitch$d,
	ubisoftAuth: ubisoftAuth$d,
	relativeDate: relativeDate$d,
	common: common$d,
	info: info$d,
	microsoft: microsoft$d,
	playButton: playButton$d,
	play: play$d,
	playMeta: playMeta$d,
	downloads: downloads$d,
	navigation: navigation$d,
	security: security$d,
	cloudSyncBehavior: cloudSyncBehavior$d,
	cloudSync: cloudSync$d,
	launchLogs: launchLogs$d,
	settings: settings$d,
	saveFolder: saveFolder$d,
	library: library$d,
	pickStorage: pickStorage$d,
	restart: restart$d,
	updater: updater$d,
	achievements: achievements$d,
	achievementsModal: achievementsModal$d,
	collectionSettings: collectionSettings$d
};

var _comment$c = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$c = {
	settings: "Ajustes",
	downloads: "Descargas"
};
var auth$c = {
	connect: "Conectar",
	disconnect: "Desconectar",
	status: {
		connected: "Conectado",
		disconnected: "No conectado",
		checking: "Comprobando…",
		error: "Error — ver registros"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$c = {
	title: "Almacenamiento",
	defaultUpdated: "Almacenamiento predeterminado actualizado",
	isDefault: "Predeterminado",
	setDefault: "Establecer como predeterminado",
	customPath: "Ruta personalizada",
	up: "↑ Subir",
	refresh: "Actualizar",
	useThisPath: "Usar esta ruta"
};
var storageSettings$c = {
	title: "Ajustes de descarga",
	installLocationLabel: "Ubicación de instalación",
	installLocationDescription: "Dónde se descargarán los nuevos juegos",
	pathLabel: "Ruta",
	loading: "Cargando opciones de almacenamiento...",
	toastUpdatedTitle: "Ubicación de almacenamiento actualizada",
	toastUpdatedBody: "Los juegos nuevos se instalarán en {{location}}",
	toastFailedTitle: "Error al actualizar",
	toastFailedBody: "{{error}}",
	unknown: "Desconocido",
	browseButton: "Examinar...",
	clearCustom: "Limpiar ubicación personalizada",
	customPathSet: "Ubicación de instalación personalizada establecida",
	newFolder: "Nueva carpeta",
	newFolderCreate: "Crear",
	selectFolder: "Usar esta carpeta",
	emptyDirectory: "Carpeta vacía",
	showHidden: "Mostrar ocultos",
	selectDevice: "Seleccionar dispositivo",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Modificado (más reciente)",
	sortModifiedOldest: "Modificado (más antiguo)",
	sortCreatedNewest: "Creado (más reciente)",
	sortCreatedOldest: "Creado (más antiguo)",
	sortSizeLargest: "Tamaño (más grande)",
	sortSizeSmallest: "Tamaño (más pequeño)",
	"validationErrors.notAbsolute": "La ruta debe ser absoluta",
	"validationErrors.systemDir": "No se puede usar un directorio del sistema",
	"validationErrors.notWritable": "La ubicación no tiene permisos de escritura",
	"validationErrors.notFound": "El directorio no existe",
	"validationErrors.homeRoot": "No se puede usar la raíz del directorio personal",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB libres)"
};
var gameGrid$c = {
	loading: "Cargando juegos...",
	noGamesFound: "No se encontraron juegos",
	noGamesHint: "Intenta sincronizar tus bibliotecas desde los ajustes de Unifideck",
	showingGames: "Mostrando {{count}} juegos",
	installed: "Instalado",
	notInstalled: "No instalado",
	playtime: "{{hours}} h {{minutes}} m jugados"
};
var downloadsTab$c = {
	currentDownload: "DESCARGA ACTUAL",
	queuedDownloads: "EN COLA ({{count}})",
	recentlyCompleted: "COMPLETADO RECIENTEMENTE",
	noActiveDownloads: "No hay descargas activas",
	noDownloads: "No hay descargas. Instala juegos desde tu biblioteca para verlos aquí.",
	loadingLabel: "Cargando...",
	loadingMessage: "Obteniendo cola de descargas...",
	confirmCancelTitle: "Confirmar cancelación",
	confirmCancelDescription: "¿Estás seguro de que deseas cancelar la descarga de {{game}}?",
	confirmCancelYes: "Sí",
	confirmCancelNo: "No",
	cancelDownload: "Cancelar",
	toastDownloadCancelledTitle: "Descarga cancelada",
	toastDownloadCancelledBody: "La descarga ha sido eliminada de la cola.",
	toastCancelFailedTitle: "Error al cancelar",
	toastCancelFailedBody: "No se pudo cancelar la descarga: {{error}}",
	status: {
		queued: "En cola",
		downloading: "Descargando",
		completed: "Completado",
		cancelled: "Cancelado",
		error: "Error"
	},
	preparingDownload: "Preparando descarga...",
	phaseExtracting: "Extrayendo {{game}}...",
	phaseVerifying: "Verificando {{game}}...",
	internalStorage: "Almacenamiento interno ({{freeSpace}} GB libres)",
	sdCard: "Tarjeta SD ({{freeSpace}} GB libres)",
	customLocation: "Personalizado: {{path}} ({{freeSpace}} GB libres)",
	extractingLabel: "Extrayendo...",
	verifyingLabel: "Verificando...",
	updateQueuedLabel: "Actualización en cola",
	downloadQueuedLabel: "Descarga en cola",
	downloadingUpdateLabel: "Descargando actualización",
	downloadingLabel: "Descargando...",
	finalizingInstallation: "Finalizando instalación...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "Tiempo restante: {{eta}}",
	preparingMessage: "Inicializando prefijo y sincronizando guardados en la nube...",
	preparingLabel: "Configurando el juego...",
	installingViaUpcLabel: "Instalando con Ubisoft Connect",
	extractingMessage: "Extrayendo archivos...",
	verifyingMessage: "Verificando archivos... {{pct}}%",
	installingViaUpcMessage: "Sigue la ventana de Ubisoft Connect para completar la instalación..."
};
var cloudSave$c = {
	title: "Conflicto de guardado en la nube",
	description: "Tus datos locales difieren de los de la nube para {{game}}",
	local: "Local:",
	cloud: "Nube:",
	newer: "(más nuevo)",
	useCloud: "Usar la nube",
	useLocal: "Usar local",
	unknownDate: "Desconocido",
	toastUsingCloudTitle: "Usando guardados en la nube",
	toastUsingCloudBody: "Descargando datos de la nube para {{game}}",
	toastUsingLocalTitle: "Usando guardados locales",
	toastUsingLocalBody: "Subiendo datos locales para {{game}}",
	toastErrorTitle: "Error",
	toastErrorBody: "Error al resolver el conflicto",
	filesSize: "{{count}} archivos · {{size}}",
	uploadBlockedEmpty: "Subida a la nube omitida para {{game}}: no se encontraron guardados locales",
	uploadSkippedTitle: "Subida a la nube omitida",
	uploadSkippedBody: "Aún no hay guardados locales para subir de {{game}}."
};
var storeConnections$c = {
	title: "CONEXIONES DE TIENDAS",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Conectado",
	checking: "Comprobando...",
	authenticate: "Iniciar sesión",
	legendaryNotInstalled: "⚠️ Instalando herramienta de Epic Games...",
	nileNotInstalled: "⚠️ Herramienta de Amazon Games no encontrada",
	error: "Error - Ver registros"
};
var librarySync$c = {
	title: "SINC. DE BIBLIOTECA",
	syncLibraries: "Sincronizar bibliotecas",
	forceSync: "Forzar sincronización",
	syncing: "Sincronizando...",
	cancelSync: "Cancelar sincronización",
	retryStatusCheck: "Reintentar comprobación de estado",
	gamesSynced: "{{synced}} / {{total}} juegos sincronizados",
	artworkDownloaded: "{{synced}} / {{total}} portadas descargadas",
	metadataExtracted: "{{synced}} / {{total}} metadatos extraídos",
	steamMetadataDownloaded: "Metadatos de Steam: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "Metadatos de unifiDB: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Datos de Metacritic: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "Metadatos de RAWG: {{synced}} / {{total}}",
	compatChecked: "Compatibilidad de datos: {{synced}} / {{total}}",
	cancelling: "Cancelando…",
	error: "Error"
};
var cleanup$c = {
	title: "Limpieza",
	deleteAll: "Eliminar todas las bibliotecas y el caché de UNIFIDECK",
	modalTitle: "¿Eliminar todos los datos de Unifideck?",
	modalDescription: "Esto eliminará todos los accesos directos de Unifideck, las portadas de Steam, las sesiones guardadas y los datos de caché. Los juegos instalados permanecerán en el disco y podrán volver a sincronizarse. Activa el modo destructivo a continuación para eliminar también los archivos de los juegos instalados.",
	deleteFilesLabel: "Eliminar también los archivos de juegos instalados (Destructivo)",
	deleteFilesDescription: "Elimina el directorio de instalación en el disco de cada juego de Unifideck dentro de una ruta segura.",
	confirmDelete: "Sí, eliminar todo",
	cancel: "Cancelar",
	deleting: "Eliminando..."
};
var captureLogs$c = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (es-ES).",
	title: "Capturar registros",
	description: "Reúne todos los registros de Unifideck en un único archivo zip en tu carpeta de Descargas, para que puedas adjuntarlo a un informe de error.",
	capture: "Capturar registros",
	capturing: "Recopilando registros...",
	savedTo: "Guardado en tu carpeta de Descargas",
	bundleInfo: "{{files}} archivos · {{size}}",
	partialNote: "No se pudo leer {{count}} archivo. El paquete sigue siendo útil.",
	partialNote_other: "No se pudieron leer {{count}} archivos. El paquete sigue siendo útil.",
	copyPath: "Copiar ruta",
	modalTitle: "¿Crear un paquete de diagnóstico?",
	modalDescription: "Unifideck reunirá sus registros en un único archivo zip en tu carpeta de Descargas, para que puedas adjuntarlo a un informe de error.",
	modalIncludes: "Incluye: registros del plugin y de los lanzamientos de juegos, historial de sincronización e instalación, los títulos de tus juegos y sus rutas de instalación, y datos de tu dispositivo como el modelo, el sistema operativo y el almacenamiento.",
	modalExcludes: "Nunca incluye: tus contraseñas, tokens de inicio de sesión de las tiendas ni cookies del navegador.",
	modalConfirm: "Crear paquete",
	cancel: "Cancelar"
};
var installButton$c = {
	play: "Jugar",
	resume: "Reanudar",
	install: "Instalar {{title}}",
	installNative: "Instalar",
	uninstall: "Desinstalar {{title}}",
	cancel: "✖ Cancelar",
	processing: "Procesando...",
	checking: "Comprobando...",
	size: "{{size}} GB",
	update: "ACTUALIZAR",
	updateAvailable: "ACTUALIZACIÓN DISPONIBLE",
	updateAvailableDescription: "Una nueva versión está lista para instalar."
};
var confirmModals$c = {
	installTitle: "Confirmar instalación",
	installDescription: "¿Estás seguro de que quieres instalar {{title}}?",
	uninstallTitle: "Confirmar desinstalación",
	uninstallDescription: "¿Estás seguro de que quieres desinstalar {{title}}?",
	cancelTitle: "Confirmar cancelación",
	cancelDescription: "¿Estás seguro de que deseas cancelar la descarga de {{title}}?",
	forceSyncTitle: "Ajustes de sincronización forzada",
	forceSyncDescription: "¿Quieres volver a sincronizar todas las portadas? Esto sobrescribirá cualquier cambio manual que hayas hecho. Selecciona 'Conservar portadas' para descargar solo las portadas que falten.",
	resyncArtwork: "Resincronizar portadas",
	keepArtwork: "Conservar portadas",
	steamRestartTitle: "¡Sincronización completada!",
	steamRestartDescription: "Por favor, reinicia Steam para aplicar tus nuevos juegos y portadas",
	steamRestartDescriptionStore: "Por favor, reinicia Steam para aplicar tus nuevos juegos y portadas de {{store}}",
	steamRestartCleanupTitle: "¡Limpieza completada!",
	steamRestartCleanupDescription: "Por favor, reinicia Steam para eliminar por completo los juegos y portadas borrados",
	restartNow: "Reiniciar ahora",
	later: "Más tarde",
	ubisoftInstallTitle: "Se requiere Ubisoft Connect",
	ubisoftInstallDescription: "Esto cargará el lanzador Ubisoft Connect, el cual necesitarás para instalar {{title}}.",
	ubisoftInstallConfirm: "Abrir Ubisoft Connect",
	yes: "Sí",
	no: "No",
	amazonUpdateTitle: "Se requiere una descarga completa",
	amazonUpdateDescription: "Las actualizaciones de Amazon requieren volver a descargar por completo {{title}}. Esto puede tardar un tiempo. ¿Continuar?",
	updateConfirm: "Actualizar"
};
var toasts$c = {
	downloadStarted: "Descarga iniciada. Comprueba UNIFIDECK > Descargas",
	downloadQueued: "Descarga en cola",
	downloadQueuedBody: "{{title}} ha sido añadido a la cola.",
	downloadFailedUnknown: "Error desconocido.",
	updateQueued: "Actualización en cola",
	uninstallDone: "Desinstalación completada.",
	ubisoftLauncherOpening: "Abriendo Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "Se está abriendo Ubisoft Connect para {{title}}. Instala el juego allí y luego regresa para jugar.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect ya está abierto para esta instalación.",
	ubisoftLauncherOpenFailed: "No se pudo abrir Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Error al abrir Ubisoft Connect para la instalación.",
	multipartDetected: "Descarga de varias partes detectada",
	multipartMessage: "Por favor, ten paciencia y espera a que termine",
	downloadFailed: "Error de descarga",
	downloadFailedMessage: "No se pudo iniciar la descarga.",
	downloadCancelled: "Descarga cancelada",
	downloadCancelledMessage: "Descarga de {{title}} cancelada.",
	cancelFailed: "Error al cancelar",
	cancelFailedMessage: "No se pudo cancelar la descarga.",
	uninstalling: "Desinstalando...",
	uninstallingMessage: "Desinstalando {{title}}...",
	uninstallingMessageProton: "Desinstalando {{title}} y archivos de Proton...",
	uninstallComplete: "Desinstalación completada",
	uninstallCompleteMessage: "{{title}} eliminado.",
	uninstallCompleteMessageProton: "{{title}} eliminado (incluidos los archivos de Proton).",
	installComplete: "¡Instalación completada!",
	installCompleteMessage: "{{title}} está listo para jugar.",
	syncComplete: "¡Sincronización completada!",
	syncCompleteMessage: "Se añadieron {{count}} juegos. REINICIA STEAM (sal por completo, no solo vuelvas al modo juego) para verlos en tu biblioteca.",
	forceSyncComplete: "¡Sincronización forzada completada!",
	forceSyncCompleteMessage: "Se actualizaron {{count}} juegos. REINICIA STEAM para ver los cambios.",
	syncCancelled: "Sincronización cancelada",
	authTimeout: "Tiempo de espera de autenticación agotado",
	authTimeoutMessage: "La autenticación de {{store}} ha caducado. Inténtalo de nuevo.",
	authFailed: "Error de autenticación",
	authFailedMessage: "Error al iniciar la autenticación",
	authError: "Error de autenticación",
	authConnected: "¡{{store}} conectado!",
	authConnectedMessage: "Autenticado correctamente con {{store}}",
	cleanupStarted: "Limpieza iniciada",
	cleanupStartedMessage: "Eliminando accesos directos, portadas y credenciales guardadas. Esto puede tardar un momento…",
	cleanupSuccessful: "Limpieza completada con éxito",
	cleanupSuccessfulMessage: "Se eliminaron {{games}} accesos directos, {{artwork}} archivos de portadas, se cerró sesión en {{stores}} tiendas y se borraron {{files}} directorios de instalación. Caché limpia.",
	cleanupNoop: "Nada que limpiar",
	cleanupNoopMessage: "Unifideck no tiene accesos directos, portadas ni credenciales guardadas que eliminar.",
	deleteFailed: "Error al eliminar",
	repairPrefixSuccess: "Prefijo reparado",
	repairPrefixSuccessMessage: "Se ha reconstruido el prefijo de Ubisoft Connect",
	repairPrefixFailed: "Error al reparar",
	repairPrefixFailedMessage: "No se pudo reparar el prefijo: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Error de red",
		launchRetry: "Reintentando inicio",
		retryingUmu: "Reintentando UMU en {{seconds}}s (intento {{attempt}}/{{max}})...",
		protonUpgrade: "Actualización de Proton",
		resettingPrefix: "Restableciendo prefijo para {{version}}...",
		firstTimeSetup: "Configuración inicial",
		initializingPrefix: "Inicializando prefijo de Wine...",
		setupCompleteTitle: "Configuración completada",
		prefixInitialized: "Prefijo de Wine inicializado",
		setupFallback: "Configuración",
		fallbackInitialization: "Inicialización alternativa...",
		launchingGame: "Iniciando juego",
		startingEpicGame: "Iniciando juego de Epic...",
		startingAmazonGame: "Iniciando juego de Amazon...",
		startingGogGame: "Iniciando juego de GOG...",
		startingUbisoftGame: "Iniciando juego de Ubisoft...",
		dependenciesTitle: "Instalando dependencias",
		installingRedistributables: "Instalando librerías redistribuibles...",
		dependenciesReady: "Dependencias listas",
		redistributablesInstalled: "Librerías redistribuibles instaladas",
		dependenciesStatus: "Dependencias",
		checkLogs: "Configuración completada (ver registros para detalles)",
		installingProton: "Instalando Proton",
		attemptingInstall: "Intentando instalar {{version}}...",
		epicAuthRequired: "Inicio de sesión de Epic requerido",
		epicAuthRequiredMessage: "Inicia sesión en Epic Games en los ajustes de Unifideck",
		installingRedist: "Instalando redistribuibles",
		installingRedistMessage: "Ejecutando instalación inicial de redistribuibles de GOG...",
		setupFailed: "Error de configuración",
		setupFailedMessage: "La instalación falló, ver registros para detalles",
		prerequisitesTitle: "Instalando requisitos previos",
		installingPrerequisites: "Instalando requisitos previos del juego...",
		prerequisitesReady: "Requisitos listos",
		prerequisitesInstalled: "Requisitos previos del juego instalados",
		prerequisitesFailed: "Advertencia de requisitos previos",
		prerequisitesFailedMessage: "Es posible que los requisitos previos no se hayan instalado correctamente",
		browserRequired: "Se requiere Microsoft Edge",
		signingIn: "Iniciando sesión...",
		cloudSyncDownFailed: "Error al descargar datos de la nube para {{store}} ({{error}}). Jugando con datos locales.",
		cloudSyncUpFailed: "Error al subir datos de la nube para {{store}} ({{error}}). Tu progreso solo está guardado localmente.",
		errorCircuitBreakerOpen: "{{game_key}} no se pudo iniciar {{count}} veces recientemente. Intentos automáticos pausados.",
		launcherError: "No se pudo iniciar {{game_key}} ({{error_code}}). Ver registros para detalles.",
		downloadingProton: "Descargando Proton: esto puede tardar unos minutos...",
		downloadingRuntime: "Descargando Steam Runtime: el primer inicio puede tardar unos minutos...",
		installingUbisoft: "Instalando con Ubisoft Connect",
		installingUbisoftMessage: "Abriendo Ubisoft Connect: inicia la instalación desde la ventana del lanzador.",
		protonReadyBody: "{{version}} está listo para usar",
		protonReadyTitle: "Proton listo",
		protonSwitchedTo: "Ahora se usa {{version}}",
		signingInUbisoft: "Iniciando sesión en Ubisoft Connect",
		signingInUbisoftMessage: "Abriendo Ubisoft Connect: inicia sesión allí y luego vuelve.",
		ubisoftPrefixNotReady: "Ubisoft Connect no está listo",
		ubisoftPrefixNotReadyMessage: "No se pudo preparar el prefijo de Ubisoft Connect para este juego. Intenta instalarlo de nuevo; si sigue fallando, vuelve a ejecutar una sincronización de la biblioteca.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect no está listo",
		ubisoftAuthPrefixNotReadyMessage: "No se pudo preparar Ubisoft Connect para el inicio de sesión. Vuelve a intentarlo en un momento.",
		ubisoftLaunchIdMissing: "Abriendo Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "No se pudo resolver el ID de inicio de este juego: se abrirá Ubisoft Connect para que puedas iniciarlo desde tu biblioteca. Vuelve a ejecutar una sincronización de la biblioteca para corregir el inicio directo."
	},
	updateComplete: "¡Actualización completada!",
	updateCompleteMessage: "{{title}} ha sido actualizado.",
	updateCancelled: "Actualización cancelada",
	updateCancelledMessage: "Se canceló la actualización de {{title}}.",
	updateFailed: "Error al actualizar",
	updateFailedMessage: "Error al iniciar la actualización.",
	storeError: "Error al comunicar con {{store}} ({{errType}})",
	captureLogsStarted: "Recopilando registros...",
	captureLogsStartedMessage: "Creando un paquete de diagnóstico en tu carpeta de Descargas. Tardará unos segundos.",
	captureLogsDone: "Registros capturados",
	captureLogsDoneMessage: "Guardado en {{path}} ({{size}}, {{files}} archivos).",
	captureLogsPartial: "Registros capturados (parcial)",
	captureLogsPartialMessage: "Guardado en {{path}} ({{size}}, {{files}} archivos). No se pudieron leer {{failures}} archivo(s).",
	captureLogsFailed: "No se pudieron capturar los registros",
	captureLogsBusy: "Ya hay una captura de registros en curso",
	captureLogsCopied: "Ruta copiada",
	captureLogsCopyFailed: "No se pudo copiar la ruta",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Liberar espacio",
		signInToStore: "Iniciar sesión",
		retrySync: "Reintentar",
		retryLibrarySync: "Actualizar biblioteca",
		openSaveFolder: "Ver carpeta de guardado",
		showLogs: "Ver registros"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "No se pudo actualizar la biblioteca de {{store}} ({{error}}). Tu biblioteca actual seguirá visible."
	},
	cloudPullStarted: "Descargando guardado en la nube…",
	cloudPushStarted: "Subiendo guardado local…",
	cloudSavePathFailed: "No se pudo establecer la ubicación de guardado",
	cloudSavePathSet: "Ubicación de guardado actualizada",
	cloudPullDone: "Guardado en la nube descargado",
	cloudPullFailed: "Error al descargar el guardado en la nube",
	cloudPushDone: "Guardado local subido",
	cloudPushFailed: "Error al subir el guardado local"
};
var uninstallModal$c = {
	title: "Confirmar desinstalación",
	description: "¿Estás seguro de que quieres desinstalar {{title}}?",
	deleteProtonLabel: "Eliminar también archivos de Proton",
	deleteProtonDescription: "Elimina los datos de compatibilidad y archivos del usuario",
	warningTitle: "Advertencia:",
	warningBody: "Esto eliminará permanentemente los datos de guardado dentro del prefijo de Wine. Asegúrate de tener una copia de seguridad.",
	cancel: "Cancelar",
	uninstall: "Desinstalar"
};
var languageSettings$c = {
	title: "IDIOMA",
	label: "Idioma de visualización",
	description: "Idioma de la interfaz y descargas de juegos (si está soportado)",
	autoDetect: "Automático (Sistema)"
};
var errors$c = {
	legendaryNotFound: "Epic Games CLI (Legendary) no encontrado",
	gogdlNotFound: "GOG CLI (gogdl) no encontrado",
	nileNotFound: "Amazon Games CLI (Nile) no encontrado",
	authUrlFailed: "No se pudo obtener la URL de autenticación",
	authFailed: "Error de autenticación",
	notAuthenticated: "No autenticado. Por favor, inicia sesión primero.",
	syncInProgress: "Ya hay una sincronización en curso",
	syncCancelled: "Sincronización cancelada por el usuario",
	gameNotFound: "Juego no encontrado",
	gameNotInstalled: "El juego no está instalado",
	installFailed: "Error de instalación - ver registros para detalles",
	installNoDirectory: "Instalación completada pero no se pudo localizar la carpeta del juego",
	uninstallFailed: "Error al desinstalar",
	lockConflict: "Conflicto de bloqueo - por favor inténtalo de nuevo",
	unknownStore: "Tienda desconocida",
	shortcutWriteFailed: "Error al escribir accesos directos",
	backgroundSyncDisabled: "Sincronización en segundo plano desactivada",
	steamGridDbUnavailable: "Biblioteca de SteamGridDB no disponible",
	deleteSyncInProgress: "No se puede eliminar mientras haya una sincronización activa",
	alreadyInQueue: "Ya está en la cola",
	download: {
		diskSpace: "No hay suficiente espacio en disco disponible",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Error de red — comprueba tu conexión",
		authExpired: "Sesión caducada — inicia sesión de nuevo",
		toolMissing: "Herramienta de descarga requerida no encontrada",
		processFailed: "Error de descarga — por favor inténtalo de nuevo",
		directoryNotFound: "No se pudieron localizar los archivos instalados",
		lockConflict: "Otra operación está en curso — inténtalo de nuevo",
		ubisoftClient: "Error del cliente Ubisoft Connect — intenta reparar el prefijo",
		ubisoftClientNotRunning: "El cliente Ubisoft Connect no responde",
		ubisoftBootstrap: "Error al configurar el prefijo de Ubisoft Connect",
		ubisoftUmuMissing: "no se encontró umu-run — requerido para juegos de Ubisoft",
		generic: "La instalación falló — ver registros para detalles"
	},
	unknown: "Error desconocido",
	steamAppsUnavailable: "API SteamClient.Apps no disponible"
};
var sync$c = {
	start: "Sincronizar bibliotecas",
	force: "Forzar sincronización",
	syncing: "Sincronizando…",
	cancel: "Cancelar sincronización",
	cancelling: "Cancelando…",
	preparing: "Preparando…",
	downloadingArtwork: "Descargando portada para {{game}}...",
	fetchingGameLists: "Obteniendo listas de juegos...",
	fetchingStore: "Obteniendo {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Sincronización cancelada por el usuario",
	checkingInstalledGames: "Comprobando juegos instalados...",
	sgdbLookup: "Buscando {{count}} juegos en SteamGridDB...",
	extractingSteamMetadata: "Extrayendo metadatos de Steam...",
	fetchingEnhancedMetadata: "Obteniendo metadatos mejorados...",
	lookingUpUnifiDB: "Obteniendo metadatos de unifiDB...",
	fetchingMetacriticData: "Obteniendo metadatos de Metacritic...",
	fetchingCompatData: "Comprobando datos de compatibilidad...",
	checkingExistingArtwork: "Comprobando portadas existentes...",
	cleaningOrphanedArtwork: "Limpiando portadas huérfanas...",
	retryingMissingArtwork: "Reintentando {{synced}} / {{total}} juegos sin portadas...",
	savingShortcuts: "Guardando accesos directos...",
	completed: "¡Sincronización completada!",
	extractingMetadata: "Extrayendo metadatos para {{game}}...",
	cooldown: "Espera {{secs}}s",
	artworkPhase: "Portadas {{done}} / {{total}}"
};
var force_sync$c = {
	migratingOldInstallations: "Sincronización forzada: Migrando instalaciones antiguas...",
	fetchingGameLists: "Sincronización forzada: Obteniendo listas de juegos...",
	checkingInstalledGames: "Sincronización forzada: Comprobando juegos instalados...",
	rewritingShortcuts: "Sincronización forzada: Reescribiendo accesos directos...",
	cancelledShortcutsSaved: "Sincronización forzada cancelada - accesos directos guardados",
	writingShortcuts: "Sincronización forzada: Escribiendo accesos directos...",
	clearingProton: "Sincronización forzada: Limpiando compatibilidad de Proton...",
	completed: "¡Sincronización forzada completada! Actualizados {{updated}} juegos, obtenidas {{artwork}} portadas."
};
var artwork$c = {
	checking: "Comprobando portadas...",
	queueRefresh: "Poniendo en cola todos los juegos para actualizar portadas...",
	downloadProgress: "Portadas descargadas: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$c = {
	greatOnDeck: "Genial en Deck",
	allGames: "Todos los juegos",
	installed: "Instalado",
	nonSteam: "No Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$c = {
	error: "Error de Unifideck",
	errorLoadingView: "Error al cargar la vista de biblioteca unificada",
	checkConsole: "Comprueba la consola del navegador para más detalles",
	errorLoadingGames: "Error al cargar juegos",
	tryReloading: "Intenta recargar el plugin o comprueba la consola para más detalles",
	storeLabel: "Tienda:",
	allStores: "Todas las tiendas",
	searchPlaceholder: "Buscar juegos...",
	library: "Biblioteca"
};
var gameInfoPanel$c = {
	loading: "Cargando información del juego...",
	noMetadata: "No hay metadatos disponibles para este juego.",
	noTestResults: "No hay resultados de pruebas detallados disponibles.",
	buttons: {
		details: "Detalles",
		synopsis: "Sinopsis",
		storePage: "Página de la tienda",
		dlc: "DLC",
		communityHub: "Punto de encuentro",
		pointsShop: "Tienda de puntos",
		discussions: "Foros de discusión",
		guides: "Guías",
		support: "Soporte",
		install: "Instalar",
		uninstall: "Desinstalar",
		cancel: "Cancelar",
		achievements: "Logros"
	},
	labels: {
		store: "Tienda:",
		name: "Nombre:",
		size: "Tamaño:",
		developer: "Desarrollador:",
		publisher: "Distribuidor:",
		released: "Lanzamiento:",
		metacritic: "Metacritic:",
		status: "Estado:",
		spaceRequired: "Espacio requerido",
		lastPlayed: "Última partida",
		playTime: "Tiempo de juego",
		never: "Nunca"
	},
	compatibility: {
		modalTitle: "Compatibilidad con Steam Deck",
		viewOnProtonDb: "Ver en ProtonDB",
		close: "Cerrar",
		verified: "VERIFICADO",
		playable: "JUGABLE",
		unsupported: "NO COMPATIBLE",
		unknown: "DESCONOCIDO"
	},
	failedToLoadMetadata: "Error al cargar metadatos",
	lastSession: "Última sesión: desbloqueado {{names}}"
};
var gameDetailsSettings$c = {
	title: "VISTA DE DETALLES DEL JUEGO",
	simple: "Simple",
	detailed: "Detallada"
};
var gogLanguageModal$c = {
	title: "Seleccionar idioma",
	description: "Elige qué idioma descargar para {{title}}",
	label: "Idioma",
	install: "Instalar",
	cancel: "Cancelar"
};
var accountSwitch$c = {
	title: "Cambio de cuenta detectado",
	description: "Se ha iniciado sesión con una cuenta de Steam diferente. ¿Qué te gustaría hacer con tus datos de Unifideck?",
	migrate: "Migrar datos de Unifideck",
	migrateDescription: "Copiar juegos y portadas de la cuenta anterior",
	freshStart: "Comenzar de cero",
	freshStartDescription: "Limpiar inicios de sesión de tiendas para conectar diferentes cuentas de Epic/GOG/Amazon/Ubisoft",
	skip: "Omitir",
	toastMigrateTitle: "Migración completada",
	toastMigrateBody: "Se migraron {{shortcuts}} accesos directos y {{artwork}} archivos de portadas",
	toastClearTitle: "Tokens de autenticación limpiados",
	toastClearBody: "Se han limpiado los inicios de sesión de las tiendas. Ahora puedes iniciar sesión con cuentas diferentes."
};
var ubisoftAuth$c = {
	title: "Inicio de sesión de Ubisoft Connect",
	description: "Inicia sesión a través de Ubisoft Connect para vincular tu biblioteca.",
	signInViaUpc: "Iniciar sesión mediante Ubisoft Connect",
	upcDescription: "Ubisoft Connect se abrirá para que inicies sesión. Una vez que lo hagas, tu sesión se capturará automáticamente.",
	launchingUpc: "Iniciando Ubisoft Connect...",
	waitingForLogin: "Esperando inicio de sesión...",
	retryLaunch: "Reintentar",
	cancel: "Cancelar",
	launchFailed: "Error al iniciar Ubisoft Connect. Por favor, inténtalo de nuevo.",
	signingIn: "Iniciando sesión...",
	emailLabel: "Correo electrónico",
	passwordLabel: "Contraseña",
	signIn: "Iniciar sesión",
	twoFactorTitle: "Autenticación de dos factores",
	twoFactorDescription: "Introduce el código de verificación de tu aplicación de autenticación.",
	twoFactorLabel: "Código de verificación",
	verify: "Verificar",
	verifying: "Verificando...",
	errorEmptyFields: "Por favor, introduce tu correo electrónico y contraseña",
	errorEmpty2FA: "Por favor, introduce el código de verificación",
	errorLoginFailed: "Error de inicio de sesión. Por favor, comprueba tus credenciales.",
	errorInvalid2FA: "Código de verificación no válido. Por favor, inténtalo de nuevo.",
	showPassword: "Mostrar contraseña",
	hidePassword: "Ocultar contraseña",
	doubleLoginWarning: "Deberás iniciar sesión dos veces para Ubisoft."
};
var relativeDate$c = {
	today: "Hoy",
	yesterday: "Ayer"
};
var common$c = {
	thisGame: "este juego",
	game: "Juego",
	unknown: "Desconocido",
	ok: "Aceptar",
	cancel: "Cancelar",
	working: "Trabajando…",
	loading: "Cargando…",
	free: "gratis",
	close: "Cerrar"
};
var info$c = {
	expandDetails: "Mostrar detalles",
	collapseDetails: "Ocultar detalles"
};
var microsoft$c = {
	notCompatible: "Este formato de juego no es compatible con SteamOS",
	playOnCloud: "Jugar en la nube",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Inicia sesión con tu cuenta de Microsoft / Xbox",
	accountConnected: "Cuenta de Microsoft conectada",
	loggedOut: "Sesión cerrada en Microsoft Store",
	xcloudNotInstalled: "Los juegos de xCloud no están instalados localmente",
	chromiumRequired: "Se requiere Microsoft Edge",
	chromiumRequiredMessage: "El inicio de sesión requiere Microsoft Edge. ¿Deseas instalarlo automáticamente? (Esto solo debe hacerse una vez).",
	chromiumInstallButton: "Instalar Microsoft Edge",
	chromiumInstalling: "Instalando Microsoft Edge...",
	chromiumInstalled: "¡Microsoft Edge instalado!",
	chromiumAlreadyInstalled: "Ya hay un navegador compatible instalado",
	chromiumInstallFailed: "Error al instalar Microsoft Edge. Por favor, instálalo manualmente: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Se agotó el tiempo de instalación de Microsoft Edge. Por favor, comprueba tu conexión a Internet.",
	browserAlreadyInstalled: "Ya hay un navegador compatible instalado",
	browserInstalled: "¡Microsoft Edge instalado correctamente!",
	browserInstallFailed: "Error al instalar Microsoft Edge. Por favor, instálalo manualmente: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak no está disponible en este sistema",
	signInRequired: "Inicia sesión para jugar",
	syncSkippedNoSubscription: "No se detectó ninguna suscripción activa a Xbox Game Pass. No se sincronizó tu biblioteca de xCloud.",
	syncSkippedTierUnknown: "Se detectó tu suscripción a Xbox Game Pass pero no se pudo determinar el plan. Sincronización omitida para evitar listar juegos no disponibles.",
	subscriptionCheckFailed: "No se pudo verificar tu suscripción a Xbox Game Pass. Sincronización de xCloud omitida. Reinténtalo cuando tu conexión se estabilice.",
	subscriptionDetected: "Suscripción activa a Xbox Game Pass {{tier}}."
};
var playButton$c = {
	play: "Jugar",
	install: "Instalar",
	cancel: "Cancelar",
	retry: "Reintentar",
	downloadFailed: "Descarga fallida",
	installing: "Instalando",
	controllerConfig: "Configuración del mando",
	appSettings: "Ajustes de la aplicación"
};
var play$c = {
	play: "Jugar",
	resume: "Reanudar",
	stop: "Detener",
	cancel: "Cancelar",
	cancelling: "Cancelando…",
	update: "Actualizar",
	uninstall: "Desinstalar",
	queued: "En cola",
	downloading: "Descargando",
	extracting: "Extrayendo",
	verifying: "Verificando",
	finalizing: "Finalizando instalación",
	cancelConfirmTitle: "¿Cancelar descarga?",
	cancelConfirmBody: "¿Estás seguro de que deseas cancelar la descarga de {{title}}? Se descartarán los archivos parciales.",
	cancelConfirmConfirm: "Cancelar descarga",
	cancelConfirmCancel: "Seguir descargando",
	playOnCloud: "Jugar en la nube",
	cloudSave: {
		autoPullOn: "Los guardados en la nube se descargan automáticamente al iniciar.",
		close: "Cerrar",
		download: "Descargar guardado en la nube",
		filesCount: "{{count}} archivos",
		label: "Guardados en la nube",
		menuTitle: "Guardados en la nube — {{game}}",
		noCloudData: "Desconocido",
		noCloudSupport: "Sin soporte de guardado en la nube",
		setLocation: "Definir ubicación de guardado",
		setLocationHint: "Elige la carpeta donde este juego guarda sus partidas.",
		setLocationTitle: "Elige la carpeta de guardado de este juego",
		statusCloudAvailable: "Guardado en la nube disponible",
		statusNoSaves: "Sin guardados locales",
		statusSyncing: "Sincronizando guardados en la nube…",
		statusUnresolved: "Ubicación de guardado no encontrada — la sincronización podría no funcionar para este juego.",
		unresolvedHint: "Inicia el juego una vez para que podamos detectar su carpeta de guardado, o define la ubicación manualmente.",
		upload: "Subir guardado local"
	},
	exe: {
		browse: "Buscar…",
		changeFailed: "No se pudo cambiar el ejecutable",
		changed: "Ejecutable cambiado",
		defaultTag: "(predeterminado)",
		menuItem: "Cambiar ejecutable…",
		reset: "Restablecer al ejecutable predeterminado",
		resetButton: "Restablecer al predeterminado",
		subtitle: "Elige qué archivo inicia este juego (por ejemplo, omitir un lanzador y ejecutar el juego directamente). El directorio de trabajo y los guardados en la nube no se verán afectados.",
		title: "Cambiar ejecutable — {{game}}",
		unavailable: "No se pudo leer la carpeta de este juego."
	}
};
var playMeta$c = {
	spaceRequired: "Espacio requerido",
	installedSize: "Tamaño instalado",
	lastPlayed: "Última partida",
	neverPlayed: "Nunca jugado",
	hoursShort: "h",
	minutesShort: "m",
	played: "Jugados"
};
var downloads$c = {
	title: "Descargas",
	empty: "No hay descargas. Instala juegos desde tu biblioteca para verlos aquí.",
	current: "Descargando ahora",
	queued: "En cola",
	finished: "Terminado recientemente",
	cancel: "Cancelar",
	removeFromQueue: "Eliminar",
	play: "Jugar",
	outcome: {
		queued: "En cola",
		downloading: "Descargando",
		completed: "Completado",
		cancelled: "Cancelado",
		error: "Fallido"
	}
};
var navigation$c = {
	loading: "Cargando...",
	storesTitle: "Tiendas",
	syncTitle: "Sinc. de biblioteca",
	languageTitle: "Idioma"
};
var security$c = {
	panelTitle: "Seguridad",
	counters: {
		label: "Contadores",
		empty: "No se han observado eventos desde el inicio"
	},
	log: {
		label: "Registro de auditoría",
		entryCount: "{{count}} entrada",
		entryCount_other: "{{count}} entradas",
		emptyFiltered: "Ningún evento coincide con el filtro"
	},
	filter: {
		label: "Filtro",
		all: "Todos los eventos"
	},
	timeline: {
		label: "Línea temporal de 24h ({{count}} evento)",
		label_other: "Línea temporal de 24h ({{count}} eventos)",
		axisStart: "-24h",
		axisEnd: "ahora"
	},
	relativeTime: {
		seconds: "hace {{count}}s",
		minutes: "hace {{count}} min",
		hours: "hace {{count}}h",
		days: "hace {{count}}d"
	},
	bruteforce: {
		warningTitle: "Advertencia: detección de fuerza bruta (nivel de advertencia)",
		escalationTitle: "ALERTE: detección de fuerza bruta (nivel de escalada)",
		detail: "{{failures}} fallos de descifrado en los últimos {{window}}s. Umbrales: {{warning}} (advertencia) / {{escalation}} (escalada).",
		resetButton: "Confirmar y restablecer el detector"
	},
	actions: {
		clearLog: "Limpiar registro y contadores"
	},
	events: {
		tokenEncrypted: "Token cifrado",
		tokenDecrypted: "Token descifrado",
		decryptFailed: "Fallo al descifrar",
		tokenFileMigrated: "Archivo de tokens migrado",
		legacyPlaintextDetected: "Texto plano antiguo detectado",
		authFlowStarted: "Autenticación iniciada",
		authFlowCompleted: "Autenticación completada",
		authFlowFailed: "Autenticación fallida",
		permissionsCheck: "Permisos comprobados",
		permissionsRepaired: "Permisos reparados",
		bruteforceSuspected: "Sospecha de fuerza bruta",
		deviceResetDetected: "Reinicio de dispositivo detectado",
		fingerprintInitialized: "Huella digital inicializada",
		externalAuthCheckFailed: "Error en comprobación de autenticación externa",
		configValidationCompleted: "Validación de configuración completada",
		configValidationFailed: "Validación de configuración fallida"
	},
	configValidation: {
		degradedTitle: "Configuración no válida: modo degradado activo",
		detail: "{{count}} error de validación detectado al iniciar",
		detail_other: "{{count}} errores de validación detectados al iniciar",
		sourceDefaults: "valores predeterminados",
		sourceUser: "ajustes del usuario",
		sourceMerged: "fusionado",
		overflow: "+ {{count}} error más",
		overflow_other: "+ {{count}} errores más"
	}
};
var cloudSyncBehavior$c = {
	_comment: "Panel de ajustes que permite al usuario elegir, por tienda, cómo deben mostrarse los fallos de sincronización de guardado en la nube. Las cinco entradas de store_label reflejan las filas representadas por CloudSyncBehaviorPanel.tsx. Microsoft se omite deliberadamente: el streaming de xCloud no tiene guardado local que sincronizar.",
	title: "Comportamiento de sinc. de guardado en la nube",
	description: "Elige cómo debe reaccionar Unifideck cuando falla una descarga o subida de guardado en la nube al iniciar un juego.",
	loadError: "No se pudieron cargar los ajustes actuales",
	saveError: "No se pudo guardar tu cambio. Por favor, inténtalo de nuevo",
	store_label: {
		"default": "Predeterminado (todas las tiendas)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Silencioso (solo registro)",
		toast: "Notificar (mensaje emergente)"
	}
};
var cloudSync$c = {
	_comment: "Mensajes emergentes emitidos por el lanzador cuando falla una sincronización en la nube. El marcador de posición del error se resuelve mediante cloudSync.error.<code> a través del campo error_i18n_key del payload del backend.",
	error: {
		network_unreachable: "Red no disponible",
		auth_expired: "Autenticación caducada",
		forbidden: "Acceso denegado",
		quota_exceeded: "Cuota de almacenamiento en la nube excedida",
		server_error: "Servidor de la tienda no disponible",
		timed_out: "Tiempo de espera de la solicitud agotado",
		disk_full: "No hay suficiente espacio en disco",
		disk_space_low: "Omitido: espacio en disco bajo (libera espacio e inténtalo de nuevo)",
		permission_denied: "Permiso denegado",
		cancelled: "Cancelado",
		unknown: "Error inesperado"
	},
	unavailableNativeDep: "Los guardados en la nube están desactivados en este sistema — a su Python ({{python}}) le falta un componente requerido. Tus partidas locales no se verán afectadas; reconstruye/reinstala el complemento después de una actualización del sistema para restaurar la sincronización."
};
var launchLogs$c = {
	_comment: "Mensajes de LaunchLogsModal. El modal se abre desde la acción de notificación unifideck://show-logs/<launch_id>, que se adjunta a los avisos de fallo del lanzador (circuit breaker + LauncherError). Lee los registros archivados de cada inicio en ~/.local/share/unifideck/launches/<launch_id>.log y los muestra con colores según el nivel de error.",
	title: "Registros de inicio — {{launchId}}",
	close: "Cerrar",
	copyAll: "Copiar todo",
	"export": "Exportar a archivo",
	copied: "Copiado al portapapeles",
	copyFailed: "Fallo al copiar",
	exporting: "Exportando…",
	exportedTo: "Exportado a {{path}}",
	exportFailed: "Error de exportación: {{error}}",
	loading: "Cargando registros…",
	notAvailable: "Los registros de este inicio ya no están disponibles (fuera del periodo de retención de 7 días).",
	error: "No se pudieron leer los registros: {{error}}",
	fileInfo: "{{path}} — mostrando {{shown}} de {{total}} líneas",
	filterLabel: "Mostrar",
	filterInfo: "Información",
	filterWarning: "Advertencia",
	filterError: "Error",
	nextError: "Siguiente error ({{current}}/{{total}})",
	noErrors: "Sin errores",
	allFiltered: "Todas las líneas ocultas por el filtro actual. Activa al menos un nivel para ver las entradas del registro."
};
var settings$c = {
	_comment: "Controles de acción de UnifideckSettingsPanel que no están cubiertos por componentes más específicos (LanguageSelector, CloudSyncBehaviorPanel). refreshAll es un botón que activa sync_all() en todas las tiendas registradas; la vista de la Biblioteca muestra el progreso de sincronización mediante su propia suscripción a eventos SYNC_PROGRESS.",
	storeConnections: "Conexiones de tiendas",
	librarySync: "Sinc. de biblioteca",
	language: "Idioma",
	languageAuto: "Detectar automáticamente",
	refreshAll: {
		sectionTitle: "Acciones de biblioteca",
		button: "Actualizar todas las bibliotecas",
		buttonBusy: "Iniciando…",
		startedTitle: "Actualización de biblioteca iniciada",
		startedBody: "Sincronizando todas las tiendas en segundo plano.",
		startedSingle: "Actualizando la biblioteca de {{store}} en segundo plano.",
		failedTitle: "No se pudo iniciar la actualización de biblioteca"
	}
};
var saveFolder$c = {
	_comment: "Mensajes para SaveFolderModal. El modal se abre desde la acción de notificación unifideck://open-save-folder/<store>/<game_id>, normalmente mostrada en fallos de sincronización en la nube donde el usuario quiere inspeccionar qué se guarda localmente. count y size se interpolan; truncated se muestra solo si el resultado fue limitado.",
	title: "Carpeta de guardado — {{store}} / {{gameId}}",
	close: "Cerrar",
	copyPath: "Copiar ruta",
	copyFileList: "Copiar lista de archivos",
	depthLabel: "Profundidad",
	filterLabel: "Filtro",
	loading: "Cargando contenido de la carpeta…",
	noSavesYet: "No se encontraron guardados locales para este juego todavía. La carpeta se creará en la próxima sincronización en la nube correcta.",
	aggregate: "{{count}} archivos, {{size}} en total",
	truncated: "{{count}} archivos más ({{size}}) no mostrados. Reduce tu filtro para verlos.",
	error: "No se pudo leer la carpeta de guardado: {{error}}"
};
var library$c = {
	_comment: "Servicios de interfaz de usuario por juego en la vista de biblioteca, incluyendo el indicador de disyuntor y controles de inicio forzado / reinicio cuando los inicios fallan repetidamente.",
	circuitBreaker: {
		_comment: "Espacio para el disyuntor de fallos de inicio. El indicador aparece solo cuando los fallos en la ventana configurada alcanzan circuit_breaker.failures_threshold (predeterminado 3 en 600s). El inicio forzado activa una omisión de un solo uso; Restablecer borra todo el historial de fallos.",
		badgeRecent: "{{count}} fallo de inicio reciente",
		badgeRecent_other: "{{count}} fallos de inicio recientes",
		badgeTooltip: "{{count}} inicios fallidos en el ventana temporal reciente. El disyuntor ha pausado los reintentos automáticos.",
		forceLaunchButton: "Forzar inicio",
		resetButton: "Restablecer fallos",
		failureKind: {
			fastBoot: "Cuelgue de inicio rápido",
			launcherError: "Error de preparación del inicio"
		}
	}
};
var pickStorage$c = {
	title: "Ubicación de instalación",
	customOption: "Elegir otra ubicación...",
	customLabel: "Ubicación personalizada",
	change: "Cambiar...",
	gameSize: "Tamaño de instalación"
};
var restart$c = {
	title: "¡Sincronización completada!",
	body: "Por favor, reinicia Steam para aplicar tus nuevos juegos y portadas",
	confirm: "Reiniciar ahora",
	later: "Más tarde"
};
var updater$c = {
	checkButton: "Buscar actualizaciones",
	checkCompleteMessage: "Información de la última versión obtenida correctamente.",
	checkCompleteTitle: "Búsqueda de actualizaciones completa",
	checkFailedTitle: "Comprobación fallida",
	checkingButton: "Comprobando...",
	currentTitle: "Actual",
	downgradeButton: "Downgrade a v{{version}}",
	installFailedTitle: "Error de instalación",
	installTimeoutMessage: "Sin respuesta de Decky Loader — es posible que la instalación se haya bloqueado o haya fallado. Inténtalo de nuevo.",
	installedLabel: "instalado",
	installingButton: "Instalando...",
	installingTitle: "Instalando plugin",
	latestLabel: "última",
	modalTitle: "UNIFIDECK v{{version}} — Notas de versión",
	noBackend: "El backend de Decky no está disponible.",
	noReleaseNotes: "No hay notas de versión disponibles.",
	reinstallButton: "Reinstalar v{{version}}",
	refreshFailedMessage: "No se pudo verificar la información de la última versión. Comprueba tu conexión e inténtalo de nuevo.",
	releaseGoneMessage: "Esta versión ya no está disponible. Busca actualizaciones y selecciónala de nuevo.",
	releaseNotesButton: "Notas de versión",
	stageDownload: "Descargando…",
	stageFinish: "Finalizando…",
	stageInstall: "Instalando…",
	stageOpen: "Leyendo paquete…",
	stageParse: "Verificando…",
	stageRemove: "Eliminando versión anterior…",
	stageStart: "Iniciando…",
	stageWorking: "Instalando…",
	titleLoading: "Comprobando versión",
	typeDowngrade: "Haciendo downgrade a",
	typeReinstall: "Reinstalando",
	typeUpdate: "Actualizando a",
	updateButton: "Actualizar a v{{version}}"
};
var achievements$c = {
	unlockedTitle: "Logro desbloqueado",
	unlockedBody: "{{name}}"
};
var achievementsModal$c = {
	title: "Logros",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Actualizar",
	close: "Cerrar",
	loading: "Cargando logros…",
	empty: "Este juego no tiene logros.",
	hidden: "Logro oculto",
	unlockedOn: "Desbloqueado el {{date}}",
	error: "No se pudieron cargar los logros.",
	errorOffline: "Conéctate a internet para ver los logros.",
	errorAuth: "Inicia sesión en la tienda de este juego para ver los logros.",
	errorUnavailable: "Los logros no están disponibles para este juego."
};
var collectionSettings$c = {
	title: "COLECCIONES DE STEAM",
	enabled: "Colecciones activadas",
	disabled: "Colecciones desactivadas"
};
var esES = {
	_comment: _comment$c,
	tabs: tabs$c,
	auth: auth$c,
	storage: storage$c,
	storageSettings: storageSettings$c,
	gameGrid: gameGrid$c,
	downloadsTab: downloadsTab$c,
	cloudSave: cloudSave$c,
	storeConnections: storeConnections$c,
	librarySync: librarySync$c,
	cleanup: cleanup$c,
	captureLogs: captureLogs$c,
	installButton: installButton$c,
	confirmModals: confirmModals$c,
	toasts: toasts$c,
	uninstallModal: uninstallModal$c,
	languageSettings: languageSettings$c,
	errors: errors$c,
	sync: sync$c,
	force_sync: force_sync$c,
	artwork: artwork$c,
	deckTabs: deckTabs$c,
	unifiedLibrary: unifiedLibrary$c,
	gameInfoPanel: gameInfoPanel$c,
	gameDetailsSettings: gameDetailsSettings$c,
	gogLanguageModal: gogLanguageModal$c,
	accountSwitch: accountSwitch$c,
	ubisoftAuth: ubisoftAuth$c,
	relativeDate: relativeDate$c,
	common: common$c,
	info: info$c,
	microsoft: microsoft$c,
	playButton: playButton$c,
	play: play$c,
	playMeta: playMeta$c,
	downloads: downloads$c,
	navigation: navigation$c,
	security: security$c,
	cloudSyncBehavior: cloudSyncBehavior$c,
	cloudSync: cloudSync$c,
	launchLogs: launchLogs$c,
	settings: settings$c,
	saveFolder: saveFolder$c,
	library: library$c,
	pickStorage: pickStorage$c,
	restart: restart$c,
	updater: updater$c,
	achievements: achievements$c,
	achievementsModal: achievementsModal$c,
	collectionSettings: collectionSettings$c
};

var _comment$b = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$b = {
	settings: "Impostazioni",
	downloads: "Download"
};
var auth$b = {
	connect: "Connetti",
	disconnect: "Disconnetti",
	status: {
		connected: "Connesso",
		disconnected: "Non connesso",
		checking: "Verifica in corso…",
		error: "Errore — controlla i log"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$b = {
	title: "Archiviazione",
	defaultUpdated: "Archiviazione predefinita aggiornata",
	isDefault: "Predefinito",
	setDefault: "Imposta come predefinito",
	customPath: "Percorso personalizzato",
	up: "↑ Su",
	refresh: "Aggiorna",
	useThisPath: "Usa questo percorso"
};
var storageSettings$b = {
	title: "Impostazioni di download",
	installLocationLabel: "Percorso di installazione",
	installLocationDescription: "Dove verranno scaricati i nuovi giochi",
	pathLabel: "Percorso",
	loading: "Caricamento opzioni di archiviazione...",
	toastUpdatedTitle: "Percorso di archiviazione aggiornato",
	toastUpdatedBody: "I nuovi giochi verranno installati in {{location}}",
	toastFailedTitle: "Aggiornamento fallito",
	toastFailedBody: "{{error}}",
	unknown: "Sconosciuto",
	browseButton: "Sfoglia...",
	clearCustom: "Cancella percorso personalizzato",
	customPathSet: "Percorso di installazione personalizzato impostato",
	newFolder: "Nuova cartella",
	newFolderCreate: "Crea",
	selectFolder: "Usa questa cartella",
	emptyDirectory: "Cartella vuota",
	showHidden: "Mostra nascosti",
	selectDevice: "Seleziona dispositivo",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Modificato (più recente)",
	sortModifiedOldest: "Modificato (meno recente)",
	sortCreatedNewest: "Creato (più recente)",
	sortCreatedOldest: "Creato (meno recente)",
	sortSizeLargest: "Dimensioni (maggiore)",
	sortSizeSmallest: "Dimensioni (minore)",
	"validationErrors.notAbsolute": "Il percorso deve essere assoluto",
	"validationErrors.systemDir": "Impossibile utilizzare una directory di sistema",
	"validationErrors.notWritable": "Il percorso non è scrivibile",
	"validationErrors.notFound": "La directory non esiste",
	"validationErrors.homeRoot": "Impossibile utilizzare la directory home principale",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB disponibili)"
};
var gameGrid$b = {
	loading: "Caricamento giochi...",
	noGamesFound: "Nessun gioco trovato",
	noGamesHint: "Prova a sincronizzare le tue librerie dalle impostazioni di Unifideck",
	showingGames: "Mostrando {{count}} giochi",
	installed: "Installato",
	notInstalled: "Non installato",
	playtime: "{{hours}}h {{minutes}}m giocati"
};
var downloadsTab$b = {
	currentDownload: "DOWNLOAD CORRENTE",
	queuedDownloads: "IN CODA ({{count}})",
	recentlyCompleted: "COMPLETATI DI RECENTE",
	noActiveDownloads: "Nessun download attivo",
	noDownloads: "Nessun download. Installa i giochi dalla tua libreria per vederli qui.",
	loadingLabel: "Caricamento...",
	loadingMessage: "Recupero della coda di download...",
	confirmCancelTitle: "Conferma annullamento",
	confirmCancelDescription: "Sei sicuro di voler annullare il download di {{game}}?",
	confirmCancelYes: "Sì",
	confirmCancelNo: "No",
	cancelDownload: "Annulla",
	toastDownloadCancelledTitle: "Download annullato",
	toastDownloadCancelledBody: "Il download è stato rimosso dalla coda.",
	toastCancelFailedTitle: "Annullamento fallito",
	toastCancelFailedBody: "Impossibile annullare il download: {{error}}",
	status: {
		queued: "In coda",
		downloading: "Download in corso",
		completed: "Completato",
		cancelled: "Annullato",
		error: "Errore"
	},
	preparingDownload: "Preparazione del download...",
	phaseExtracting: "Estrazione di {{game}}...",
	phaseVerifying: "Verifica di {{game}}...",
	internalStorage: "Memoria interna ({{freeSpace}} GB disponibili)",
	sdCard: "Scheda SD ({{freeSpace}} GB disponibili)",
	customLocation: "Personalizzato: {{path}} ({{freeSpace}} GB disponibili)",
	extractingLabel: "Estrazione...",
	verifyingLabel: "Verifica...",
	updateQueuedLabel: "Aggiornamento in coda",
	downloadQueuedLabel: "Download in coda",
	downloadingUpdateLabel: "Download dell'aggiornamento",
	downloadingLabel: "Download in corso...",
	finalizingInstallation: "Finalizzazione dell'installazione...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "Tempo stimato: {{eta}}",
	preparingMessage: "Inizializzazione del prefisso e sincronizzazione dei salvataggi cloud...",
	preparingLabel: "Configurazione del gioco...",
	installingViaUpcLabel: "Installazione tramite Ubisoft Connect",
	extractingMessage: "Estrazione dei file...",
	verifyingMessage: "Verifica dei file... {{pct}}%",
	installingViaUpcMessage: "Segui la finestra di Ubisoft Connect per completare l'installazione..."
};
var cloudSave$b = {
	title: "Conflitto di salvataggio cloud",
	description: "I tuoi salvataggi locali differiscono da quelli cloud per {{game}}",
	local: "Locale:",
	cloud: "Cloud:",
	newer: "(più recente)",
	useCloud: "Usa cloud",
	useLocal: "Usa locale",
	unknownDate: "Sconosciuto",
	toastUsingCloudTitle: "Utilizzo dei salvataggi cloud",
	toastUsingCloudBody: "Download dei salvataggi cloud per {{game}}",
	toastUsingLocalTitle: "Utilizzo dei salvataggi locali",
	toastUsingLocalBody: "Caricamento dei salvataggi locali per {{game}}",
	toastErrorTitle: "Errore",
	toastErrorBody: "Risoluzione del conflitto fallita",
	filesSize: "{{count}} file · {{size}}",
	uploadBlockedEmpty: "Caricamento sul cloud ignorato per {{game}} — nessun salvataggio locale trovato",
	uploadSkippedTitle: "Caricamento sul cloud ignorato",
	uploadSkippedBody: "Ancora nessun salvataggio locale da caricare per {{game}}."
};
var storeConnections$b = {
	title: "CONNESSIONI NEGOZIO",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Connesso",
	checking: "Verifica...",
	authenticate: "Accedi",
	legendaryNotInstalled: "⚠️ Installazione di Epic Games CLI...",
	nileNotInstalled: "⚠️ Amazon Games CLI non trovato",
	error: "Errore - Controlla i log"
};
var librarySync$b = {
	title: "SINCRONIZZAZIONE LIBRERIA",
	syncLibraries: "Sincronizza librerie",
	forceSync: "Forza sincronizzazione",
	syncing: "Sincronizzazione...",
	cancelSync: "Annulla sincronizzazione",
	retryStatusCheck: "Riprova controllo di stato",
	gamesSynced: "{{synced}} / {{total}} giochi sincronizzati",
	artworkDownloaded: "{{synced}} / {{total}} copertine scaricate",
	metadataExtracted: "{{synced}} / {{total}} record di metadati estratti",
	steamMetadataDownloaded: "Metadati di Steam: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "Metadati di unifiDB: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Dati di Metacritic: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "Metadati di RAWG: {{synced}} / {{total}}",
	compatChecked: "Dati di compatibilità: {{synced}} / {{total}}",
	cancelling: "Annullamento…",
	error: "Errore"
};
var cleanup$b = {
	title: "Pulizia",
	deleteAll: "Elimina tutte le librerie e la cache di UNIFIDECK",
	modalTitle: "Eliminare tutti i dati di Unifideck?",
	modalDescription: "Questo rimuoverà ogni collegamento Unifideck, copertine Steam, accessi salvati e dati di cache. I giochi installati rimarranno sul disco e potrai risincronizzarli. Abilita la modalità distruttiva qui sotto per eliminare anche i file dei giochi installati.",
	deleteFilesLabel: "Elimina anche i file dei giochi installati (Distruttivo)",
	deleteFilesDescription: "Rimuove la directory di installazione sul disco per ogni gioco Unifideck all'interno di un percorso sicuro.",
	confirmDelete: "Sì, elimina tutto",
	cancel: "Annulla",
	deleting: "Eliminazione..."
};
var captureLogs$b = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (it-IT).",
	title: "Acquisisci registri",
	description: "Raccoglie tutti i registri di Unifideck in un unico file zip nella cartella Download, così puoi allegarlo a una segnalazione di bug.",
	capture: "Acquisisci registri",
	capturing: "Raccolta dei registri...",
	savedTo: "Salvato nella cartella Download",
	bundleInfo: "{{files}} file · {{size}}",
	partialNote: "Non è stato possibile leggere {{count}} file. Il pacchetto è comunque utilizzabile.",
	partialNote_other: "Non è stato possibile leggere {{count}} file. Il pacchetto è comunque utilizzabile.",
	copyPath: "Copia percorso",
	modalTitle: "Creare un pacchetto diagnostico?",
	modalDescription: "Unifideck raccoglierà i suoi registri in un unico file zip nella cartella Download, così potrai allegarlo a una segnalazione di bug.",
	modalIncludes: "Incluso: registri del plugin e degli avvii dei giochi, cronologia di sincronizzazione e installazione, i titoli dei tuoi giochi e i relativi percorsi, e informazioni sul dispositivo come modello, sistema operativo e archiviazione.",
	modalExcludes: "Mai incluso: le tue password, i token di accesso agli store o i cookie del browser.",
	modalConfirm: "Crea pacchetto",
	cancel: "Annulla"
};
var installButton$b = {
	play: "Gioca",
	resume: "Riprendi",
	install: "Installa {{title}}",
	installNative: "Installa",
	uninstall: "Disinstalla {{title}}",
	cancel: "✖ Annulla",
	processing: "Elaborazione...",
	checking: "Verifica...",
	size: "{{size}} GB",
	update: "AGGIORNA",
	updateAvailable: "AGGIORNAMENTO DISPONIBILE",
	updateAvailableDescription: "Una nuova versione è pronta per essere installata."
};
var confirmModals$b = {
	installTitle: "Conferma installazione",
	installDescription: "Sei sicuro di voler installare {{title}}?",
	uninstallTitle: "Conferma disinstallazione",
	uninstallDescription: "Sei sicuro di voler disinstallare {{title}}?",
	cancelTitle: "Conferma annullamento",
	cancelDescription: "Sei sicuro di voler annullare il download di {{title}}?",
	forceSyncTitle: "Impostazioni di sincronizzazione forzata",
	forceSyncDescription: "Vuoi risincronizzare tutte le copertine? Questo sovrascriverà qualsiasi modifica manuale apportata alle copertine. Seleziona 'Mantieni copertine' per scaricare solo quelle mancanti.",
	resyncArtwork: "Risincronizza copertine",
	keepArtwork: "Mantieni copertine",
	steamRestartTitle: "Sincronizzazione completata!",
	steamRestartDescription: "Riavvia Steam per applicare i tuoi nuovi giochi e copertine",
	steamRestartDescriptionStore: "Riavvia Steam per applicare i tuoi nuovi giochi e copertine di {{store}}",
	steamRestartCleanupTitle: "Pulizia completata!",
	steamRestartCleanupDescription: "Riavvia Steam per rimuovere completamente i giochi e le copertine eliminati",
	restartNow: "Riavvia ora",
	later: "Più tardi",
	ubisoftInstallTitle: "Ubisoft Connect richiesto",
	ubisoftInstallDescription: "Questo caricherà l'avvio di Ubisoft Connect che ti servirà per installare {{title}}.",
	ubisoftInstallConfirm: "Apri Ubisoft Connect",
	yes: "Sì",
	no: "No",
	amazonUpdateTitle: "Download completo richiesto",
	amazonUpdateDescription: "Gli aggiornamenti di Amazon richiedono di scaricare nuovamente tutto {{title}}. Questo potrebbe richiedere tempo. Continuare?",
	updateConfirm: "Aggiorna"
};
var toasts$b = {
	downloadStarted: "Download avviato. Controlla UNIFIDECK > Download",
	downloadQueued: "Download in coda",
	downloadQueuedBody: "{{title}} è stato aggiunto alla coda.",
	downloadFailedUnknown: "Errore sconosciuto.",
	updateQueued: "Aggiornamento in coda",
	uninstallDone: "Disinstallazione completata.",
	ubisoftLauncherOpening: "Apertura di Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect si sta aprendo per {{title}}. Installa il gioco lì, poi torna per giocare.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect è già aperto per questa installazione.",
	ubisoftLauncherOpenFailed: "Impossibile aprire Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Apertura di Ubisoft Connect fallita per l'installazione.",
	multipartDetected: "Rilevato download multi-parte",
	multipartMessage: "Si prega di pazientare e attendere il completamento",
	downloadFailed: "Download fallito",
	downloadFailedMessage: "Impossibile avviare il download.",
	downloadCancelled: "Download annullato",
	downloadCancelledMessage: "Download di {{title}} annullato.",
	cancelFailed: "Annullamento fallito",
	cancelFailedMessage: "Impossibile annullare il download.",
	uninstalling: "Disinstallazione...",
	uninstallingMessage: "Disinstallazione di {{title}}...",
	uninstallingMessageProton: "Disinstallazione di {{title}} e dei file Proton...",
	uninstallComplete: "Disinstallazione completata",
	uninstallCompleteMessage: "{{title}} rimosso.",
	uninstallCompleteMessageProton: "{{title}} rimosso (inclusi i file Proton).",
	installComplete: "Installazione completata!",
	installCompleteMessage: "{{title}} è pronto per giocare.",
	syncComplete: "Sincronizzazione completata!",
	syncCompleteMessage: "Aggiunti {{count}} giochi. RIAVVIA STEAM (esci completamente, non solo tornare in modalità gioco) per vederli nella tua libreria.",
	forceSyncComplete: "Sincronizzazione forzata completata!",
	forceSyncCompleteMessage: "Aggiornati {{count}} giochi. RIAVVIA STEAM per vedere le modifiche.",
	syncCancelled: "Sincronizzazione annullata",
	authTimeout: "Timeout di autenticazione",
	authTimeoutMessage: "Autenticazione di {{store}} scaduta. Riprova.",
	authFailed: "Autenticazione fallita",
	authFailedMessage: "Avvio dell'autenticazione fallito",
	authError: "Errore di autenticazione",
	authConnected: "{{store}} Connesso!",
	authConnectedMessage: "Autenticato con successo con {{store}}",
	cleanupStarted: "Pulizia avviata",
	cleanupStartedMessage: "Rimozione di collegamenti, copertine e credenziali salvate. Potrebbe richiedere un momento…",
	cleanupSuccessful: "Pulizia completata con successo",
	cleanupSuccessfulMessage: "Rimossi {{games}} collegamenti, {{artwork}} file di copertine, disconnesso da {{stores}} negozi, eliminati {{files}} percorsi d'installazione. Cache svuotata.",
	cleanupNoop: "Nulla da pulire",
	cleanupNoopMessage: "Unifideck non ha collegamenti, copertine o credenziali salvate da rimuovere.",
	deleteFailed: "Eliminazione fallita",
	repairPrefixSuccess: "Prefisso riparato",
	repairPrefixSuccessMessage: "Il prefisso Ubisoft Connect è stato ricostruito",
	repairPrefixFailed: "Riparazione fallita",
	repairPrefixFailedMessage: "Impossibile riparare il prefisso: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Errore di rete",
		launchRetry: "Nuovo tentativo di avvio",
		retryingUmu: "Riprova UMU in {{seconds}}s (tentativo {{attempt}}/{{max}})...",
		protonUpgrade: "Aggiornamento Proton",
		resettingPrefix: "Ripristino prefisso per {{version}}...",
		firstTimeSetup: "Prima configurazione",
		initializingPrefix: "Inizializzazione prefisso Wine...",
		setupCompleteTitle: "Configurazione completata",
		prefixInitialized: "Prefisso Wine inizializzato",
		setupFallback: "Configurazione",
		fallbackInitialization: "Inizializzazione di riserva...",
		launchingGame: "Avvio del gioco",
		startingEpicGame: "Avvio del gioco Epic...",
		startingAmazonGame: "Avvio del gioco Amazon...",
		startingGogGame: "Avvio del gioco GOG...",
		startingUbisoftGame: "Avvio del gioco Ubisoft...",
		dependenciesTitle: "Installazione dipendenze",
		installingRedistributables: "Installazione dei componenti redistribuibili di gioco...",
		dependenciesReady: "Dipendenze pronte",
		redistributablesInstalled: "Componenti redistribuibili installati",
		dependenciesStatus: "Dipendenze",
		checkLogs: "Configurazione completata (controlla i log per i dettagli)",
		installingProton: "Installazione Proton",
		attemptingInstall: "Tentativo di installare {{version}}...",
		epicAuthRequired: "Richiesto accesso Epic",
		epicAuthRequiredMessage: "Accedi a Epic Games nelle impostazioni di Unifideck",
		installingRedist: "Installazione redistribuibili",
		installingRedistMessage: "Esecuzione della prima installazione dei redistribuibili GOG...",
		setupFailed: "Configurazione fallita",
		setupFailedMessage: "Installazione fallita, guarda i log per dettagli",
		prerequisitesTitle: "Installazione prerequisiti",
		installingPrerequisites: "Installazione dei prerequisiti di gioco richiesti...",
		prerequisitesReady: "Prerequisiti pronti",
		prerequisitesInstalled: "Prerequisiti di gioco installati",
		prerequisitesFailed: "Avviso sui prerequisiti",
		prerequisitesFailedMessage: "I prerequisiti potrebbero non essere stati installati correttamente",
		browserRequired: "Richiesto Microsoft Edge",
		signingIn: "Accesso in corso...",
		cloudSyncDownFailed: "Download salvataggi cloud fallito per {{store}} ({{error}}). Esecuzione con salvataggi locali.",
		cloudSyncUpFailed: "Caricamento salvataggi cloud fallito per {{store}} ({{error}}). I tuoi progressi sono salvati solo in locale.",
		errorCircuitBreakerOpen: "{{game_key}} ha fallito l'avvio {{count}} volte di recente. Avvii automatici sospesi.",
		launcherError: "Impossibile avviare {{game_key}} ({{error_code}}). Controlla i log per dettagli.",
		downloadingProton: "Download di Proton in corso — potrebbe richiedere qualche minuto...",
		downloadingRuntime: "Download di Steam Runtime in corso — il primo avvio potrebbe richiedere qualche minuto...",
		installingUbisoft: "Installazione tramite Ubisoft Connect",
		installingUbisoftMessage: "Apertura di Ubisoft Connect — avvia l'installazione dalla finestra del launcher.",
		protonReadyBody: "{{version}} è pronto all'uso",
		protonReadyTitle: "Proton pronto",
		protonSwitchedTo: "Ora in uso {{version}}",
		signingInUbisoft: "Accesso a Ubisoft Connect",
		signingInUbisoftMessage: "Apertura di Ubisoft Connect — accedi lì, poi torna indietro.",
		ubisoftPrefixNotReady: "Ubisoft Connect non è pronto",
		ubisoftPrefixNotReadyMessage: "Impossibile preparare il prefix di Ubisoft Connect per questo gioco. Prova a installare di nuovo; se continua a non riuscire, esegui di nuovo una sincronizzazione della libreria.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect non è pronto",
		ubisoftAuthPrefixNotReadyMessage: "Impossibile preparare Ubisoft Connect per l'accesso. Riprova tra un istante.",
		ubisoftLaunchIdMissing: "Apertura di Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "Impossibile risolvere l'ID di avvio di questo gioco — verrà aperto Ubisoft Connect per poterlo avviare dalla tua libreria. Esegui di nuovo una sincronizzazione della libreria per correggere l'avvio diretto."
	},
	updateComplete: "Aggiornamento completato!",
	updateCompleteMessage: "{{title}} è stato aggiornato.",
	updateCancelled: "Aggiornamento annullato",
	updateCancelledMessage: "L'aggiornamento di {{title}} è stato annullato.",
	updateFailed: "Aggiornamento fallito",
	updateFailedMessage: "Avvio dell'aggiornamento fallito.",
	storeError: "Comunicazione con {{store}} fallita ({{errType}})",
	captureLogsStarted: "Raccolta dei registri...",
	captureLogsStartedMessage: "Creazione di un pacchetto diagnostico nella cartella Download. Richiede alcuni secondi.",
	captureLogsDone: "Registri acquisiti",
	captureLogsDoneMessage: "Salvato in {{path}} ({{size}}, {{files}} file).",
	captureLogsPartial: "Registri acquisiti (parziale)",
	captureLogsPartialMessage: "Salvato in {{path}} ({{size}}, {{files}} file). Non è stato possibile leggere {{failures}} file.",
	captureLogsFailed: "Impossibile acquisire i registri",
	captureLogsBusy: "Un'acquisizione dei registri è già in corso",
	captureLogsCopied: "Percorso copiato",
	captureLogsCopyFailed: "Impossibile copiare il percorso",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Libera spazio",
		signInToStore: "Accedi",
		retrySync: "Riprova",
		retryLibrarySync: "Aggiorna libreria",
		openSaveFolder: "Mostra cartella salvataggi",
		showLogs: "Mostra log"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Impossibile aggiornare la libreria {{store}} ({{error}}). La tua libreria esistente rimarrà visibile."
	},
	cloudPullStarted: "Download del salvataggio cloud…",
	cloudPushStarted: "Caricamento del salvataggio locale…",
	cloudSavePathFailed: "Impossibile impostare la cartella di salvataggio",
	cloudSavePathSet: "Cartella di salvataggio aggiornata",
	cloudPullDone: "Salvataggio cloud scaricato",
	cloudPullFailed: "Download del salvataggio cloud non riuscito",
	cloudPushDone: "Salvataggio locale caricato",
	cloudPushFailed: "Caricamento del salvataggio locale non riuscito"
};
var uninstallModal$b = {
	title: "Conferma disinstallazione",
	description: "Sei sicuro di voler disinstallare {{title}}?",
	deleteProtonLabel: "Elimina anche i file Proton",
	deleteProtonDescription: "Rimuove i dati di compatibilità e i file utente",
	warningTitle: "Attenzione:",
	warningBody: "Questo eliminerà permanentemente i salvataggi archiviati nel prefisso Wine. Assicurati di aver fatto un backup.",
	cancel: "Annulla",
	uninstall: "Disinstalla"
};
var languageSettings$b = {
	title: "LINGUA",
	label: "Lingua dell'interfaccia",
	description: "Lingua per l'interfaccia utente e i download dei giochi (dove supportato)",
	autoDetect: "Auto (Sistema)"
};
var errors$b = {
	legendaryNotFound: "Epic Games CLI (Legendary) non trovato",
	gogdlNotFound: "GOG CLI (gogdl) non trovato",
	nileNotFound: "Amazon Games CLI (Nile) non trovato",
	authUrlFailed: "Impossibile ottenere l'URL di autenticazione",
	authFailed: "Autenticazione fallita",
	notAuthenticated: "Non autenticato. Effettua prima l'accesso.",
	syncInProgress: "Una sincronizzazione è già in corso",
	syncCancelled: "Sincronizzazione annullata dall'utente",
	gameNotFound: "Gioco non trovato",
	gameNotInstalled: "Il gioco non è installato",
	installFailed: "Installazione fallita - controlla i log per dettagli",
	installNoDirectory: "Installazione completata ma impossibile individuare la directory di gioco",
	uninstallFailed: "Disinstallazione fallita",
	lockConflict: "Conflitto di blocco - riprova più tardi",
	unknownStore: "Negozio sconosciuto",
	shortcutWriteFailed: "Scrittura collegamenti fallita",
	backgroundSyncDisabled: "Sincronizzazione in background disattivata",
	steamGridDbUnavailable: "Libreria SteamGridDB non disponibile",
	deleteSyncInProgress: "Impossibile eliminare mentre è in corso una sincronizzazione",
	alreadyInQueue: "Già in coda",
	download: {
		diskSpace: "Spazio su disco insufficiente",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Errore di rete — controlla la connessione",
		authExpired: "Sessione scaduta — effettua nuovamente l'accesso",
		toolMissing: "Strumento di download richiesto non trovato",
		processFailed: "Download fallito — riprova più tardi",
		directoryNotFound: "Impossibile trovare i file installati",
		lockConflict: "Un'altra operazione è in corso — riprova",
		ubisoftClient: "Errore client Ubisoft Connect — prova a riparare il prefisso",
		ubisoftClientNotRunning: "Il client Ubisoft Connect non risponde",
		ubisoftBootstrap: "Impossibile configurare il prefisso Ubisoft Connect",
		ubisoftUmuMissing: "umu-run non trovato — richiesto per i giochi Ubisoft",
		generic: "Installazione fallita — controlla i log per dettagli"
	},
	unknown: "Errore sconosciuto",
	steamAppsUnavailable: "API SteamClient.Apps non disponibile"
};
var sync$b = {
	start: "Sincronizza librerie",
	force: "Forza sincronizzazione",
	syncing: "Sincronizzazione in corso…",
	cancel: "Annulla sincronizzazione",
	cancelling: "Annullamento…",
	preparing: "Preparazione…",
	downloadingArtwork: "Download copertina per {{game}}...",
	fetchingGameLists: "Recupero elenchi giochi...",
	fetchingStore: "Recupero {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Sincronizzazione annullata dall'utente",
	checkingInstalledGames: "Verifica giochi installati...",
	sgdbLookup: "Ricerca di {{count}} giochi su SteamGridDB...",
	extractingSteamMetadata: "Estrazione metadati Steam...",
	fetchingEnhancedMetadata: "Recupero metadati avanzati...",
	lookingUpUnifiDB: "Recupero metadati unifiDB...",
	fetchingMetacriticData: "Recupero metadati Metacritic...",
	fetchingCompatData: "Verifica dati di compatibilità...",
	checkingExistingArtwork: "Verifica copertine esistenti...",
	cleaningOrphanedArtwork: "Pulizia copertine orfane...",
	retryingMissingArtwork: "Riprova {{synced}} / {{total}} giochi con copertine mancanti...",
	savingShortcuts: "Salvataggio collegamenti...",
	completed: "Sincronizzazione completata!",
	extractingMetadata: "Estrazione metadati per {{game}}...",
	cooldown: "Attendi {{secs}}s",
	artworkPhase: "Copertina {{done}} / {{total}}"
};
var force_sync$b = {
	migratingOldInstallations: "Sincronizzazione forzata: Migrazione vecchie installazioni...",
	fetchingGameLists: "Sincronizzazione forzata: Recupero elenchi giochi...",
	checkingInstalledGames: "Sincronizzazione forzata: Verifica giochi installati...",
	rewritingShortcuts: "Sincronizzazione forzata: Riscrittura collegamenti...",
	cancelledShortcutsSaved: "Sincronizzazione forzata annullata - collegamenti salvati",
	writingShortcuts: "Sincronizzazione forzata: Scrittura collegamenti...",
	clearingProton: "Sincronizzazione forzata: Pulizia compatibilità Proton...",
	completed: "Sincronizzazione forzata completata! Aggiornati {{updated}} giochi, recuperate {{artwork}} copertine."
};
var artwork$b = {
	checking: "Verifica copertine...",
	queueRefresh: "Accodamento di tutti i giochi per l'aggiornamento copertine...",
	downloadProgress: "Copertine scaricate: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$b = {
	greatOnDeck: "Ottimi su Deck",
	allGames: "Tutti i giochi",
	installed: "Installato",
	nonSteam: "Non Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$b = {
	error: "Errore Unifideck",
	errorLoadingView: "Impossibile caricare la vista della libreria unificata",
	checkConsole: "Controlla la console del browser per i dettagli",
	errorLoadingGames: "Errore nel caricamento dei giochi",
	tryReloading: "Prova a ricaricare il plugin o controlla la console per i dettagli",
	storeLabel: "Negozio:",
	allStores: "Tutti i negozi",
	searchPlaceholder: "Cerca giochi...",
	library: "Libreria"
};
var gameInfoPanel$b = {
	loading: "Caricamento informazioni gioco...",
	noMetadata: "Nessun metadato disponibile per questo gioco.",
	noTestResults: "Nessun risultato di test dettagliato disponibile.",
	buttons: {
		details: "Dettagli",
		synopsis: "Sinossi",
		storePage: "Pagina del negozio",
		dlc: "DLC",
		communityHub: "Hub della comunità",
		pointsShop: "Negozio dei punti",
		discussions: "Discussioni",
		guides: "Guide",
		support: "Supporto",
		install: "Installa",
		uninstall: "Disinstalla",
		cancel: "Annulla",
		achievements: "Obiettivi"
	},
	labels: {
		store: "Negozio:",
		name: "Nome:",
		size: "Dimensione:",
		developer: "Sviluppatore:",
		publisher: "Editore:",
		released: "Rilasciato:",
		metacritic: "Metacritic:",
		status: "Stato:",
		spaceRequired: "Spazio richiesto",
		lastPlayed: "Ultima partita",
		playTime: "Tempo di gioco",
		never: "Mai"
	},
	compatibility: {
		modalTitle: "Compatibilità Steam Deck",
		viewOnProtonDb: "Visualizza su ProtonDB",
		close: "Chiudi",
		verified: "VERIFICATO",
		playable: "GIOCABILE",
		unsupported: "NON SUPPORTATO",
		unknown: "SCONOSCIUTO"
	},
	failedToLoadMetadata: "Impossibile caricare i metadati",
	lastSession: "Ultima sessione: sbloccato {{names}}"
};
var gameDetailsSettings$b = {
	title: "VISTA DETTAGLI GIOCO",
	simple: "Semplice",
	detailed: "Dettagliata"
};
var gogLanguageModal$b = {
	title: "Seleziona lingua",
	description: "Scegli quale lingua scaricare per {{title}}",
	label: "Lingua",
	install: "Installa",
	cancel: "Annulla"
};
var accountSwitch$b = {
	title: "Rilevato cambio di account",
	description: "È stato effettuato l'accesso con un account Steam diverso. Cosa vorresti fare con i tuoi dati Unifideck?",
	migrate: "Migra i dati Unifideck",
	migrateDescription: "Copia i giochi e le copertine dall'account precedente",
	freshStart: "Inizio da zero",
	freshStartDescription: "Cancella gli accessi ai negozi per connettere diversi account Epic/GOG/Amazon/Ubisoft",
	skip: "Salta",
	toastMigrateTitle: "Migrazione completata",
	toastMigrateBody: "Migrati {{shortcuts}} collegamenti e {{artwork}} file di copertine",
	toastClearTitle: "Token di autenticazione cancellati",
	toastClearBody: "Gli accessi ai negozi sono stati cancellati. Ora puoi accedere con account diversi."
};
var ubisoftAuth$b = {
	title: "Accesso a Ubisoft Connect",
	description: "Accedi tramite Ubisoft Connect per collegare la tua libreria.",
	signInViaUpc: "Accedi tramite Ubisoft Connect",
	upcDescription: "Ubisoft Connect si aprirà per consentirti l'accesso. Una volta effettuato l'accesso, la tua sessione verrà catturata automaticamente.",
	launchingUpc: "Avvio di Ubisoft Connect...",
	waitingForLogin: "In attesa dell'accesso...",
	retryLaunch: "Riprova",
	cancel: "Annulla",
	launchFailed: "Impossibile avviare Ubisoft Connect. Riprova.",
	signingIn: "Accesso in corso...",
	emailLabel: "E-mail",
	passwordLabel: "Password",
	signIn: "Accedi",
	twoFactorTitle: "Autenticazione a due fattori",
	twoFactorDescription: "Inserisci il codice di verifica della tua app di autenticazione.",
	twoFactorLabel: "Codice di verifica",
	verify: "Verifica",
	verifying: "Verifica in corso...",
	errorEmptyFields: "Inserisci e-mail e password",
	errorEmpty2FA: "Inserisci il codice di verifica",
	errorLoginFailed: "Accesso fallito. Verifica le tue credenziali.",
	errorInvalid2FA: "Codice di verifica non valido. Riprova.",
	showPassword: "Mostra password",
	hidePassword: "Nascondi password",
	doubleLoginWarning: "Dovrai accedere due volte per Ubisoft."
};
var relativeDate$b = {
	today: "Oggi",
	yesterday: "Ieri"
};
var common$b = {
	thisGame: "questo gioco",
	game: "Gioco",
	unknown: "Sconosciuto",
	ok: "OK",
	cancel: "Annulla",
	working: "Elaborazione…",
	loading: "Caricamento…",
	free: "gratis",
	close: "Chiudi"
};
var info$b = {
	expandDetails: "Mostra dettagli",
	collapseDetails: "Nascondi dettagli"
};
var microsoft$b = {
	notCompatible: "Questo formato di gioco non è compatibile con SteamOS",
	playOnCloud: "Gioca sul Cloud",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Accedi con il tuo account Microsoft / Xbox",
	accountConnected: "Account Microsoft connesso",
	loggedOut: "Disconnesso da Microsoft Store",
	xcloudNotInstalled: "I giochi xCloud non sono installati localmente",
	chromiumRequired: "Richiesto Microsoft Edge",
	chromiumRequiredMessage: "L'accesso richiede Microsoft Edge. Desideri installarlo automaticamente? (Questa operazione è richiesta una sola volta.)",
	chromiumInstallButton: "Installa Microsoft Edge",
	chromiumInstalling: "Installazione di Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge installato!",
	chromiumAlreadyInstalled: "Un browser compatibile è già installato",
	chromiumInstallFailed: "Installazione di Microsoft Edge fallita. Installa manualmente: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Installazione di Microsoft Edge scaduta. Controlla la tua connessione Internet.",
	browserAlreadyInstalled: "Un browser compatibile è già installato",
	browserInstalled: "Microsoft Edge installato con successo!",
	browserInstallFailed: "Installazione di Microsoft Edge fallita. Installa manualmente: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak non è disponibile su questo sistema",
	signInRequired: "Accedi per giocare",
	syncSkippedNoSubscription: "Nessun abbonamento Xbox Game Pass attivo rilevato. La tua libreria xCloud non è stata sincronizzata.",
	syncSkippedTierUnknown: "L'abbonamento Xbox Game Pass è stato rilevato ma non è stato possibile determinare il piano. Sincronizzazione saltata per evitare di elencare giochi non disponibili.",
	subscriptionCheckFailed: "Impossibile verificare l'abbonamento Xbox Game Pass. Sincronizzazione xCloud saltata. Riprova quando la connessione si stabilizza.",
	subscriptionDetected: "Abbonamento Xbox Game Pass {{tier}} attivo."
};
var playButton$b = {
	play: "Gioca",
	install: "Installa",
	cancel: "Annulla",
	retry: "Riprova",
	downloadFailed: "Download fallito",
	installing: "Installazione in corso",
	controllerConfig: "Configurazione del controller",
	appSettings: "Impostazioni dell'app"
};
var play$b = {
	play: "Gioca",
	resume: "Riprendi",
	stop: "Ferma",
	cancel: "Annulla",
	cancelling: "Annullamento…",
	update: "Aggiorna",
	uninstall: "Disinstalla",
	queued: "In coda",
	downloading: "Download in corso",
	extracting: "Estrazione in corso",
	verifying: "Verifica in corso",
	finalizing: "Finalizzazione dell'installazione",
	cancelConfirmTitle: "Annullare il download?",
	cancelConfirmBody: "Sei sicuro di voler annullare il download di {{title}}? I file parziali verranno eliminati.",
	cancelConfirmConfirm: "Annulla download",
	cancelConfirmCancel: "Continua il download",
	playOnCloud: "Gioca sul Cloud",
	cloudSave: {
		autoPullOn: "I salvataggi cloud vengono scaricati automaticamente all'avvio.",
		close: "Chiudi",
		download: "Scarica salvataggio cloud",
		filesCount: "{{count}} file",
		label: "Salvataggi cloud",
		menuTitle: "Salvataggi cloud — {{game}}",
		noCloudData: "Sconosciuto",
		noCloudSupport: "Nessun supporto al salvataggio cloud",
		setLocation: "Imposta cartella di salvataggio",
		setLocationHint: "Scegli la cartella in cui questo gioco memorizza i suoi salvataggi.",
		setLocationTitle: "Scegli la cartella di salvataggio del gioco",
		statusCloudAvailable: "Salvataggio cloud disponibile",
		statusNoSaves: "Nessun salvataggio locale",
		statusSyncing: "Sincronizzazione dei salvataggi cloud…",
		statusUnresolved: "Cartella di salvataggio non trovata — la sincronizzazione potrebbe non funzionare per questo gioco.",
		unresolvedHint: "Avvia il gioco una volta per rilevare la sua cartella di salvataggio, o impostala manualmente.",
		upload: "Carica salvataggio locale"
	},
	exe: {
		browse: "Sfoglia…",
		changeFailed: "Impossibile modificare l'eseguibile",
		changed: "Eseguibile modificato",
		defaultTag: "(predefinito)",
		menuItem: "Modifica eseguibile…",
		reset: "Ripristina l'eseguibile predefinito",
		resetButton: "Ripristina predefinito",
		subtitle: "Scegli quale file avvia questo gioco (es. saltare un launcher e avviare direttamente il gioco). La cartella di lavoro e i salvataggi cloud non subiranno variazioni.",
		title: "Modifica eseguibile — {{game}}",
		unavailable: "Impossibile leggere la cartella di questo gioco."
	}
};
var playMeta$b = {
	spaceRequired: "Spazio richiesto",
	installedSize: "Spazio occupato",
	lastPlayed: "Ultima partita",
	neverPlayed: "Mai giocato",
	hoursShort: "h",
	minutesShort: "m",
	played: "Giocati"
};
var downloads$b = {
	title: "Download",
	empty: "Nessun download. Installa i giochi dalla tua libreria per vederli qui.",
	current: "Download in corso",
	queued: "In coda",
	finished: "Completati di recente",
	cancel: "Annulla",
	removeFromQueue: "Rimuovi",
	play: "Gioca",
	outcome: {
		queued: "In coda",
		downloading: "Download in corso",
		completed: "Completato",
		cancelled: "Annullato",
		error: "Fallito"
	}
};
var navigation$b = {
	loading: "Caricamento...",
	storesTitle: "Negozi",
	syncTitle: "Sincr. libreria",
	languageTitle: "Lingua"
};
var security$b = {
	panelTitle: "Sicurezza",
	counters: {
		label: "Contatori",
		empty: "Nessun evento registrato dall'avvio"
	},
	log: {
		label: "Log di controllo",
		entryCount: "{{count}} voce",
		entryCount_other: "{{count}} voci",
		emptyFiltered: "Nessun evento corrisponde al filtro"
	},
	filter: {
		label: "Filtro",
		all: "Tutti gli eventi"
	},
	timeline: {
		label: "Cronologia 24h ({{count}} evento)",
		label_other: "Cronologia 24h ({{count}} eventi)",
		axisStart: "-24h",
		axisEnd: "ora"
	},
	relativeTime: {
		seconds: "{{count}}s fa",
		minutes: "{{count}} min fa",
		hours: "{{count}}h fa",
		days: "{{count}}g fa"
	},
	bruteforce: {
		warningTitle: "Avviso: rilevamento brute-force (livello di avviso)",
		escalationTitle: "ALLERTA: rilevamento brute-force (livello di escalation)",
		detail: "{{failures}} fallimenti di decrittografia negli ultimi {{window}}s. Soglie: {{warning}} (avviso) / {{escalation}} (escalation).",
		resetButton: "Conferma e reimposta il rilevatore"
	},
	actions: {
		clearLog: "Cancella log e contatori"
	},
	events: {
		tokenEncrypted: "Token crittografato",
		tokenDecrypted: "Token decrittografato",
		decryptFailed: "Decrittografia fallita",
		tokenFileMigrated: "File token migrato",
		legacyPlaintextDetected: "Rilevato testo in chiaro legacy",
		authFlowStarted: "Autenticazione avviata",
		authFlowCompleted: "Autenticazione completata",
		authFlowFailed: "Autenticazione fallita",
		permissionsCheck: "Permessi controllati",
		permissionsRepaired: "Permessi riparati",
		bruteforceSuspected: "Sospetto brute-force",
		deviceResetDetected: "Rilevato ripristino del dispositivo",
		fingerprintInitialized: "Impronta inizializzata",
		externalAuthCheckFailed: "Verifica autenticazione esterna fallita",
		configValidationCompleted: "Validazione configurazione completata",
		configValidationFailed: "Validazione configurazione fallita"
	},
	configValidation: {
		degradedTitle: "Configurazione non valida - modalità degradata attiva",
		detail: "{{count}} errore di validazione rilevato all'avvio",
		detail_other: "{{count}} errori di validazione rilevati all'avvio",
		sourceDefaults: "impostazioni predefinite",
		sourceUser: "modifiche utente",
		sourceMerged: "unito",
		overflow: "+ {{count}} altro errore",
		overflow_other: "+ {{count}} altri errori"
	}
};
var cloudSyncBehavior$b = {
	_comment: "Pannello delle impostazioni che consente all'utente di scegliere, per ciascun negozio, come gestire gli errori di sincronizzazione dei salvataggi cloud. Le cinque voci store_label riflettono le righe visualizzate da CloudSyncBehaviorPanel.tsx. Microsoft è volutamente omesso: lo streaming xCloud non ha salvataggi locali da sincronizzare.",
	title: "Comportamento di sincr. salvataggi cloud",
	description: "Scegli come Unifideck deve reagire quando il download o caricamento dei salvataggi cloud fallisce all'avvio del gioco.",
	loadError: "Impossibile caricare le impostazioni correnti",
	saveError: "Impossibile salvare la modifica — riprova",
	store_label: {
		"default": "Predefinito (tutti i negozi)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Silenzioso (solo log)",
		toast: "Notifica (messaggio)"
	}
};
var cloudSync$b = {
	_comment: "Messaggi di notifica emessi dal launcher quando la sincronizzazione cloud fallisce. Il segnaposto dell'errore viene risolto tramite cloudSync.error.<code> tramite il campo error_i18n_key del payload del backend.",
	error: {
		network_unreachable: "Rete non raggiungibile",
		auth_expired: "Autenticazione scaduta",
		forbidden: "Accesso negato",
		quota_exceeded: "Quota cloud superata",
		server_error: "Server del negozio non disponibile",
		timed_out: "Richiesta scaduta",
		disk_full: "Spazio su disco insufficiente",
		disk_space_low: "Saltato — spazio su disco insufficiente (libera spazio e riprova)",
		permission_denied: "Permesso negato",
		cancelled: "Annullato",
		unknown: "Errore imprevisto"
	},
	unavailableNativeDep: "I salvataggi cloud sono disattivati su questo sistema — al suo Python ({{python}}) manca un componente richiesto. I tuoi salvataggi locali non sono influenzati; ricostruisci/reinstalla il plugin dopo un aggiornamento di sistema per ripristinare la sincronizzazione."
};
var launchLogs$b = {
	_comment: "Stringhe di LaunchLogsModal. Il modal si apre dall'azione della notifica unifideck://show-logs/<launch_id>, associata alle notifiche di fallimento dell'avvio (circuit breaker + LauncherError). Legge i log archiviati per singolo avvio da ~/.local/share/unifideck/launches/<launch_id>.log e li mostra con colori in base alla gravità.",
	title: "Log di avvio — {{launchId}}",
	close: "Chiudi",
	copyAll: "Copia tutto",
	"export": "Esporta su file",
	copied: "Copiato negli appunti",
	copyFailed: "Copia fallita",
	exporting: "Esportazione in corso…",
	exportedTo: "Esportato in {{path}}",
	exportFailed: "Esportazione fallita: {{error}}",
	loading: "Caricamento log…",
	notAvailable: "I log per questo avvio non sono più disponibili (oltre il periodo di conservazione di 7 giorni).",
	error: "Impossibile leggere i log: {{error}}",
	fileInfo: "{{path}} — mostrate {{shown}} di {{total}} righe",
	filterLabel: "Mostra",
	filterInfo: "Info",
	filterWarning: "Avviso",
	filterError: "Errore",
	nextError: "Errore successivo ({{current}}/{{total}})",
	noErrors: "Nessun errore",
	allFiltered: "Tutte le righe sono nascoste dal filtro corrente. Abilita almeno un livello per visualizzare le voci di log."
};
var settings$b = {
	_comment: "Controlli d'azione di UnifideckSettingsPanel non coperti da componenti più specifici (LanguageSelector, CloudSyncBehaviorPanel). refreshAll è un pulsante che avvia sync_all() in tutti i negozi registrati; la vista Libreria mostra l'avanzamento della sincronizzazione tramite la sottoscrizione agli eventi SYNC_PROGRESS.",
	storeConnections: "Connessioni negozio",
	librarySync: "Sincr. libreria",
	language: "Lingua",
	languageAuto: "Rilevamento automatico",
	refreshAll: {
		sectionTitle: "Azioni libreria",
		button: "Aggiorna tutte le librerie",
		buttonBusy: "Avvio…",
		startedTitle: "Aggiornamento libreria avviato",
		startedBody: "Sincronizzazione di tutti i negozi in background.",
		startedSingle: "Aggiornamento della libreria {{store}} in background.",
		failedTitle: "Impossibile avviare l'aggiornamento della libreria"
	}
};
var saveFolder$b = {
	_comment: "Stringhe per SaveFolderModal. Il modal si apre dall'azione della notifica unifideck://open-save-folder/<store>/<game_id>, solitamente mostrata in caso di errore di sincronizzazione cloud quando l'utente desidera controllare cosa è effettivamente salvato in locale. count e size sono interpolati; truncated viene mostrato solo quando il risultato è limitato.",
	title: "Cartella salvataggi — {{store}} / {{gameId}}",
	close: "Chiudi",
	copyPath: "Copia percorso",
	copyFileList: "Copia elenco file",
	depthLabel: "Profondità",
	filterLabel: "Filtro",
	loading: "Caricamento contenuto cartella…",
	noSavesYet: "Ancora nessun salvataggio trovato in locale per questo gioco. La cartella verrà creata alla prossima sincronizzazione cloud riuscita.",
	aggregate: "{{count}} file, {{size}} in totale",
	truncated: "Altri {{count}} file ({{size}}) non mostrati — restringi il filtro per visualizzarli.",
	error: "Impossibile leggere la cartella dei salvataggi: {{error}}"
};
var library$b = {
	_comment: "Funzionalità dell'interfaccia utente per gioco nella vista Libreria, incluso il badge del disgiuntore di avvio e i comandi di avvio forzato / ripristino in caso di avvii falliti ripetutamente.",
	circuitBreaker: {
		_comment: "Interfaccia per il disgiuntore di errori di avvio. Il badge viene visualizzato solo quando gli errori nella finestra temporale configurata raggiungono circuit_breaker.failures_threshold (predefinito 3 in 600s). L'avvio forzato abilita un bypass singolo; Ripristina azzera completamente la cronologia dei fallimenti.",
		badgeRecent: "{{count}} errore di avvio recente",
		badgeRecent_other: "{{count}} errori di avvio recenti",
		badgeTooltip: "{{count}} avvii falliti nella finestra temporale recente — il disgiuntore ha sospeso i tentativi automatici",
		forceLaunchButton: "Forza avvio",
		resetButton: "Ripristina errori",
		failureKind: {
			fastBoot: "Crash avvio rapido",
			launcherError: "Errore configurazione avvio"
		}
	}
};
var pickStorage$b = {
	title: "Percorso d'installazione",
	customOption: "Scegli un altro percorso...",
	customLabel: "Percorso personalizzato",
	change: "Cambia...",
	gameSize: "Dimensioni installazione"
};
var restart$b = {
	title: "Sincronizzazione completata!",
	body: "Riavvia Steam per applicare i tuoi nuovi giochi e copertine",
	confirm: "Riavvia ora",
	later: "Più tardi"
};
var updater$b = {
	checkButton: "Verifica aggiornamenti",
	checkCompleteMessage: "Informazioni sulla versione più recente recuperate con successo.",
	checkCompleteTitle: "Verifica aggiornamenti completata",
	checkFailedTitle: "Verifica fallita",
	checkingButton: "Verifica...",
	currentTitle: "Attuale",
	downgradeButton: "Downgrade a v{{version}}",
	installFailedTitle: "Installazione fallita",
	installTimeoutMessage: "Nessuna risposta da Decky Loader — l'installazione potrebbe essersi bloccata o non essere riuscita. Riprova.",
	installedLabel: "installato",
	installingButton: "Installazione...",
	installingTitle: "Installazione plugin in corso",
	latestLabel: "ultima",
	modalTitle: "UNIFIDECK v{{version}} — Note di rilascio",
	noBackend: "Il backend di Decky non è disponibile.",
	noReleaseNotes: "Nessuna nota di rilascio disponibile.",
	reinstallButton: "Reinstalla v{{version}}",
	refreshFailedMessage: "Impossibile verificare le informazioni sull'ultima versione. Controlla la connessione e riprova.",
	releaseGoneMessage: "Questa versione non è più disponibile. Verifica gli aggiornamenti e selezionala di nuovo.",
	releaseNotesButton: "Note di rilascio",
	stageDownload: "Download…",
	stageFinish: "Finalizzazione…",
	stageInstall: "Installazione…",
	stageOpen: "Lettura pacchetto…",
	stageParse: "Verifica…",
	stageRemove: "Rimozione vecchia versione…",
	stageStart: "Avvio…",
	stageWorking: "Installazione…",
	titleLoading: "Verifica versione",
	typeDowngrade: "Downgrade a",
	typeReinstall: "Reinstallazione",
	typeUpdate: "Aggiornamento a",
	updateButton: "Aggiorna alla v{{version}}"
};
var achievements$b = {
	unlockedTitle: "Obiettivo sbloccato",
	unlockedBody: "{{name}}"
};
var achievementsModal$b = {
	title: "Obiettivi",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Aggiorna",
	close: "Chiudi",
	loading: "Caricamento obiettivi…",
	empty: "Questo gioco non ha obiettivi.",
	hidden: "Obiettivo nascosto",
	unlockedOn: "Sbloccato il {{date}}",
	error: "Impossibile caricare gli obiettivi.",
	errorOffline: "Connettiti a internet per vedere gli obiettivi.",
	errorAuth: "Accedi allo store di questo gioco per vedere gli obiettivi.",
	errorUnavailable: "Gli obiettivi non sono disponibili per questo gioco."
};
var collectionSettings$b = {
	title: "COLLEZIONI DI STEAM",
	enabled: "Collezioni abilitate",
	disabled: "Collezioni disabilitate"
};
var itIT = {
	_comment: _comment$b,
	tabs: tabs$b,
	auth: auth$b,
	storage: storage$b,
	storageSettings: storageSettings$b,
	gameGrid: gameGrid$b,
	downloadsTab: downloadsTab$b,
	cloudSave: cloudSave$b,
	storeConnections: storeConnections$b,
	librarySync: librarySync$b,
	cleanup: cleanup$b,
	captureLogs: captureLogs$b,
	installButton: installButton$b,
	confirmModals: confirmModals$b,
	toasts: toasts$b,
	uninstallModal: uninstallModal$b,
	languageSettings: languageSettings$b,
	errors: errors$b,
	sync: sync$b,
	force_sync: force_sync$b,
	artwork: artwork$b,
	deckTabs: deckTabs$b,
	unifiedLibrary: unifiedLibrary$b,
	gameInfoPanel: gameInfoPanel$b,
	gameDetailsSettings: gameDetailsSettings$b,
	gogLanguageModal: gogLanguageModal$b,
	accountSwitch: accountSwitch$b,
	ubisoftAuth: ubisoftAuth$b,
	relativeDate: relativeDate$b,
	common: common$b,
	info: info$b,
	microsoft: microsoft$b,
	playButton: playButton$b,
	play: play$b,
	playMeta: playMeta$b,
	downloads: downloads$b,
	navigation: navigation$b,
	security: security$b,
	cloudSyncBehavior: cloudSyncBehavior$b,
	cloudSync: cloudSync$b,
	launchLogs: launchLogs$b,
	settings: settings$b,
	saveFolder: saveFolder$b,
	library: library$b,
	pickStorage: pickStorage$b,
	restart: restart$b,
	updater: updater$b,
	achievements: achievements$b,
	achievementsModal: achievementsModal$b,
	collectionSettings: collectionSettings$b
};

var _comment$a = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$a = {
	settings: "Configurações",
	downloads: "Downloads"
};
var auth$a = {
	connect: "Conectar",
	disconnect: "Desconectar",
	status: {
		connected: "Conectado",
		disconnected: "Não conectado",
		checking: "Verificando…",
		error: "Erro — verifique os logs"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$a = {
	title: "Armazenamento",
	defaultUpdated: "Armazenamento padrão atualizado",
	isDefault: "Padrão",
	setDefault: "Definir como Padrão",
	customPath: "Caminho Personalizado",
	up: "↑ Subir",
	refresh: "Atualizar",
	useThisPath: "Usar Este Caminho"
};
var storageSettings$a = {
	title: "Configurações de Download",
	installLocationLabel: "Local de Instalação",
	installLocationDescription: "Onde os novos jogos serão baixados",
	pathLabel: "Caminho",
	loading: "Carregando opções de armazenamento...",
	toastUpdatedTitle: "Local de Armazenamento Atualizado",
	toastUpdatedBody: "Novos jogos serão instalados em {{location}}",
	toastFailedTitle: "Falha ao Atualizar",
	toastFailedBody: "{{error}}",
	unknown: "Desconhecido",
	browseButton: "Navegar...",
	clearCustom: "Limpar Local Personalizado",
	customPathSet: "Local de instalação personalizado definido",
	newFolder: "Nova Pasta",
	newFolderCreate: "Criar",
	selectFolder: "Usar Esta Pasta",
	emptyDirectory: "Pasta vazia",
	showHidden: "Mostrar Ocultos",
	selectDevice: "Selecionar Dispositivo",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Modificado (Mais recente)",
	sortModifiedOldest: "Modificado (Mais antigo)",
	sortCreatedNewest: "Criado (Mais recente)",
	sortCreatedOldest: "Criado (Mais antigo)",
	sortSizeLargest: "Tamanho (Maior)",
	sortSizeSmallest: "Tamanho (Menor)",
	"validationErrors.notAbsolute": "O caminho deve ser absoluto",
	"validationErrors.systemDir": "Não é possível usar um diretório do sistema",
	"validationErrors.notWritable": "O local não permite escrita",
	"validationErrors.notFound": "O diretório não existe",
	"validationErrors.homeRoot": "Não é possível usar a raiz do diretório do usuário",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB livres)"
};
var gameGrid$a = {
	loading: "Carregando jogos...",
	noGamesFound: "Nenhum jogo encontrado",
	noGamesHint: "Tente sincronizar suas bibliotecas nas configurações do Unifideck",
	showingGames: "Exibindo {{count}} jogos",
	installed: "Instalado",
	notInstalled: "Não instalado",
	playtime: "{{hours}}h {{minutes}}m jogados"
};
var downloadsTab$a = {
	currentDownload: "DOWNLOAD ATUAL",
	queuedDownloads: "NA FILA ({{count}})",
	recentlyCompleted: "CONCLUÍDO RECENTEMENTE",
	noActiveDownloads: "Nenhum download ativo",
	noDownloads: "Nenhum download. Instale jogos da sua biblioteca para vê-los aqui.",
	loadingLabel: "Carregando...",
	loadingMessage: "Buscando fila de downloads...",
	confirmCancelTitle: "Confirmar Cancelamento",
	confirmCancelDescription: "Tem certeza de que deseja cancelar o download de {{game}}?",
	confirmCancelYes: "Sim",
	confirmCancelNo: "Não",
	cancelDownload: "Cancelar",
	toastDownloadCancelledTitle: "Download Cancelado",
	toastDownloadCancelledBody: "O download foi removido da fila.",
	toastCancelFailedTitle: "Falha ao Cancelar",
	toastCancelFailedBody: "Não foi possível cancelar o download: {{error}}",
	status: {
		queued: "Na fila",
		downloading: "Baixando",
		completed: "Concluído",
		cancelled: "Cancelado",
		error: "Erro"
	},
	preparingDownload: "Preparando download...",
	phaseExtracting: "Extraindo {{game}}...",
	phaseVerifying: "Verificando {{game}}...",
	internalStorage: "Armazenamento Interno ({{freeSpace}} GB livres)",
	sdCard: "Cartão SD ({{freeSpace}} GB livres)",
	customLocation: "Personalizado: {{path}} ({{freeSpace}} GB livre)",
	extractingLabel: "Extraindo...",
	verifyingLabel: "Verificando...",
	updateQueuedLabel: "Atualização na Fila",
	downloadQueuedLabel: "Download na Fila",
	downloadingUpdateLabel: "Baixando Atualização",
	downloadingLabel: "Baixando...",
	finalizingInstallation: "Finalizando instalação...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "Tempo restante: {{eta}}",
	preparingMessage: "Inicializando prefixo e sincronizando salvamentos na nuvem...",
	preparingLabel: "Configurando o jogo...",
	installingViaUpcLabel: "Instalando via Ubisoft Connect",
	extractingMessage: "Extraindo arquivos...",
	verifyingMessage: "Verificando arquivos... {{pct}}%",
	installingViaUpcMessage: "Siga a janela do Ubisoft Connect para concluir a instalação..."
};
var cloudSave$a = {
	title: "Conflito de Salvamento na Nuvem",
	description: "Seus arquivos locais diferem dos salvamentos na nuvem para {{game}}",
	local: "Local:",
	cloud: "Nuvem:",
	newer: "(mais recente)",
	useCloud: "Usar a Nuvem",
	useLocal: "Usar Local",
	unknownDate: "Desconhecido",
	toastUsingCloudTitle: "Usando Salvamentos na Nuvem",
	toastUsingCloudBody: "Baixando salvamentos na nuvem para {{game}}",
	toastUsingLocalTitle: "Usando Salvamentos Locais",
	toastUsingLocalBody: "Enviando salvamentos locais para {{game}}",
	toastErrorTitle: "Erro",
	toastErrorBody: "Falha ao resolver conflito",
	filesSize: "{{count}} arquivos · {{size}}",
	uploadBlockedEmpty: "Envio para a nuvem ignorado para {{game}} — nenhum save local encontrado",
	uploadSkippedTitle: "Envio para a nuvem ignorado",
	uploadSkippedBody: "Ainda não há saves locais para enviar de {{game}}."
};
var storeConnections$a = {
	title: "CONEXÕES DE LOJAS",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Conectado",
	checking: "Verificando...",
	authenticate: "Entrar",
	legendaryNotInstalled: "⚠️ Instalando Epic Games CLI...",
	nileNotInstalled: "⚠️ Amazon Games CLI não encontrado",
	error: "Erro - Verifique os logs"
};
var librarySync$a = {
	title: "SINCRONIZAÇÃO DE BIBLIOTECA",
	syncLibraries: "Sincronizar Bibliotecas",
	forceSync: "Forçar Sincronização",
	syncing: "Sincronizando...",
	cancelSync: "Cancelar Sincronização",
	retryStatusCheck: "Repetir Verificação de Status",
	gamesSynced: "{{synced}} / {{total}} jogos sincronizados",
	artworkDownloaded: "{{synced}} / {{total}} artes de capa baixadas",
	metadataExtracted: "{{synced}} / {{total}} registros de metadados extraídos",
	steamMetadataDownloaded: "Metadados do Steam: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "Metadados do unifiDB: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Dados do Metacritic: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "Metadados do RAWG: {{synced}} / {{total}}",
	compatChecked: "Compatibilidade de dados: {{synced}} / {{total}}",
	cancelling: "Cancelando…",
	error: "Erro"
};
var cleanup$a = {
	title: "Limpeza",
	deleteAll: "Excluir todas as bibliotecas e cache do UNIFIDECK",
	modalTitle: "Excluir todos os dados do Unifideck?",
	modalDescription: "Isso removerá todos os atalhos do Unifideck, capas do Steam, logins salvos e dados de cache. Os jogos instalados continuarão no disco e podem ser sincronizados novamente. Ative o modo destrutivo abaixo para excluir também os arquivos de jogos instalados.",
	deleteFilesLabel: "Também excluir arquivos de jogos instalados (Destrutivo)",
	deleteFilesDescription: "Remove o diretório de instalação em disco de cada jogo do Unifideck sob uma pasta raiz segura.",
	confirmDelete: "Sim, Excluir Tudo",
	cancel: "Cancelar",
	deleting: "Excluindo..."
};
var captureLogs$a = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (pt-BR).",
	title: "Capturar logs",
	description: "Reúne todos os logs do Unifideck em um único arquivo zip na sua pasta Downloads, para você anexar a um relatório de bug.",
	capture: "Capturar logs",
	capturing: "Coletando logs...",
	savedTo: "Salvo na sua pasta Downloads",
	bundleInfo: "{{files}} arquivos · {{size}}",
	partialNote: "Não foi possível ler {{count}} arquivo. O pacote continua utilizável.",
	partialNote_other: "Não foi possível ler {{count}} arquivos. O pacote continua utilizável.",
	copyPath: "Copiar caminho",
	modalTitle: "Criar um pacote de diagnóstico?",
	modalDescription: "O Unifideck vai reunir seus logs em um único arquivo zip na sua pasta Downloads, para você anexar a um relatório de bug.",
	modalIncludes: "Incluído: logs do plugin e das execuções de jogos, histórico de sincronização e instalação, os títulos dos seus jogos e seus caminhos de instalação, e dados do seu dispositivo como modelo, sistema operacional e armazenamento.",
	modalExcludes: "Nunca incluído: suas senhas, tokens de login das lojas ou cookies do navegador.",
	modalConfirm: "Criar pacote",
	cancel: "Cancelar"
};
var installButton$a = {
	play: "Jogar",
	resume: "Retomar",
	install: "Instalar {{title}}",
	installNative: "Instalar",
	uninstall: "Desinstalar {{title}}",
	cancel: "✖ Cancelar",
	processing: "Processando...",
	checking: "Verificando...",
	size: "{{size}} GB",
	update: "ATUALIZAR",
	updateAvailable: "ATUALIZAÇÃO DISPONÍVEL",
	updateAvailableDescription: "Uma nova versão está pronta para instalação."
};
var confirmModals$a = {
	installTitle: "Confirmar Instalação",
	installDescription: "Tem certeza de que deseja instalar {{title}}?",
	uninstallTitle: "Confirmar Desinstalação",
	uninstallDescription: "Tem certeza de que deseja desinstalar {{title}}?",
	cancelTitle: "Confirmar Cancelamento",
	cancelDescription: "Tem certeza de que deseja cancelar o download de {{title}}?",
	forceSyncTitle: "Configurações de Sincronização Forçada",
	forceSyncDescription: "Gostaria de sincronizar novamente todas as artes? Isso substituirá quaisquer alterações manuais nas artes. Selecione 'Manter Artes' para baixar apenas artes ausentes.",
	resyncArtwork: "Sincronizar Novamente Artes",
	keepArtwork: "Manter Artes",
	steamRestartTitle: "Sincronização Concluída!",
	steamRestartDescription: "Por favor, reinicie o Steam para aplicar seus novos jogos e artes",
	steamRestartDescriptionStore: "Por favor, reinicie o Steam para aplicar seus novos jogos e artes do {{store}}",
	steamRestartCleanupTitle: "Limpeza Concluída!",
	steamRestartCleanupDescription: "Por favor, reinicie o Steam para remover completamente jogos e artes excluídos",
	restartNow: "Reiniciar Agora",
	later: "Mais Tarde",
	ubisoftInstallTitle: "Ubisoft Connect Necessário",
	ubisoftInstallDescription: "Isso carregará o inicializador do Ubisoft Connect que você precisará para instalar {{title}}.",
	ubisoftInstallConfirm: "Abrir Ubisoft Connect",
	yes: "Sim",
	no: "Não",
	amazonUpdateTitle: "Download Completo Necessário",
	amazonUpdateDescription: "As atualizações da Amazon exigem um download completo do {{title}}. Isso pode levar algum tempo. Continuar?",
	updateConfirm: "Atualizar"
};
var toasts$a = {
	downloadStarted: "Download Iniciado. Verifique UNIFIDECK > Downloads",
	downloadQueued: "Download na fila",
	downloadQueuedBody: "{{title}} foi adicionado à fila.",
	downloadFailedUnknown: "Erro desconhecido.",
	updateQueued: "Atualização na fila",
	uninstallDone: "Desinstalação concluída.",
	ubisoftLauncherOpening: "Abrindo Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "O Ubisoft Connect está abrindo para {{title}}. Instale o jogo lá e retorne para jogar.",
	ubisoftLauncherAlreadyOpenMessage: "O Ubisoft Connect já está aberto para esta instalação.",
	ubisoftLauncherOpenFailed: "Não foi possível abrir o Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Falha ao abrir o Ubisoft Connect para instalação.",
	multipartDetected: "Download em várias partes detectado",
	multipartMessage: "Por favor, seja paciente e aguarde a conclusão",
	downloadFailed: "Falha no Download",
	downloadFailedMessage: "Não foi possível iniciar o download.",
	downloadCancelled: "Download Cancelado",
	downloadCancelledMessage: "Download de {{title}} cancelado.",
	cancelFailed: "Falha ao Cancelar",
	cancelFailedMessage: "Não foi possível cancelar o download.",
	uninstalling: "Desinstalando...",
	uninstallingMessage: "Desinstalando {{title}}...",
	uninstallingMessageProton: "Desinstalando {{title}} e arquivos do Proton...",
	uninstallComplete: "Desinstalação Concluída",
	uninstallCompleteMessage: "{{title}} removido.",
	uninstallCompleteMessageProton: "{{title}} removido (incluindo arquivos do Proton).",
	installComplete: "Instalação Concluída!",
	installCompleteMessage: "{{title}} está pronto para jogar.",
	syncComplete: "Sincronização Concluída!",
	syncCompleteMessage: "Adicionado {{count}} jogos. REINICIE O STEAM (saia completamente, não apenas retorne ao modo de jogo) para vê-los em sua biblioteca.",
	forceSyncComplete: "Sincronização Forçada Concluída!",
	forceSyncCompleteMessage: "Atualizado {{count}} jogos. REINICIE O STEAM para ver as alterações.",
	syncCancelled: "Sincronização Cancelada",
	authTimeout: "Tempo Limite de Autenticação",
	authTimeoutMessage: "A autenticação do {{store}} expirou. Por favor, tente novamente.",
	authFailed: "Falha na Autenticação",
	authFailedMessage: "Falha ao iniciar autenticação",
	authError: "Erro de Autenticação",
	authConnected: "{{store}} Conectado!",
	authConnectedMessage: "Autenticado com sucesso com {{store}}",
	cleanupStarted: "Limpeza Iniciada",
	cleanupStartedMessage: "Removendo atalhos, artes e logins salvos. Isso pode levar um momento…",
	cleanupSuccessful: "Limpeza Concluída com Sucesso",
	cleanupSuccessfulMessage: "Removido {{games}} atalhos, {{artwork}} arquivos de artes, desconectado de {{stores}} lojas, excluídos {{files}} pastas de instalação. Cache limpo.",
	cleanupNoop: "Nada a Limpar",
	cleanupNoopMessage: "O Unifideck não tem atalhos, artes ou logins salvos para remover.",
	deleteFailed: "Falha ao Excluir",
	repairPrefixSuccess: "Prefixo Reparado",
	repairPrefixSuccessMessage: "O prefixo do Ubisoft Connect foi reconstruído",
	repairPrefixFailed: "Falha ao Reparar",
	repairPrefixFailedMessage: "Não foi possível reparar o prefixo: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Erro de Rede",
		launchRetry: "Tentando iniciar novamente",
		retryingUmu: "Tentando novamente o UMU em {{seconds}}s (tentativa {{attempt}}/{{max}})...",
		protonUpgrade: "Upgrade do Proton",
		resettingPrefix: "Redefinindo o prefixo para {{version}}...",
		firstTimeSetup: "Configuração Inicial",
		initializingPrefix: "Inicializando prefixo do Wine...",
		setupCompleteTitle: "Configuração Concluída",
		prefixInitialized: "Prefixo do Wine inicializado",
		setupFallback: "Configuração",
		fallbackInitialization: "Inicialização alternativa...",
		launchingGame: "Iniciando Jogo",
		startingEpicGame: "Iniciando jogo da Epic...",
		startingAmazonGame: "Iniciando jogo da Amazon...",
		startingGogGame: "Iniciando jogo do GOG...",
		startingUbisoftGame: "Iniciando jogo da Ubisoft...",
		dependenciesTitle: "Instalando Dependências",
		installingRedistributables: "Instalando redistribuíveis do jogo...",
		dependenciesReady: "Dependências Prontas",
		redistributablesInstalled: "Redistribuíveis instalados",
		dependenciesStatus: "Dependências",
		checkLogs: "Configuração concluída (verifique os logs para detalhes)",
		installingProton: "Instalando Proton",
		attemptingInstall: "Tentando instalar {{version}}...",
		epicAuthRequired: "Login da Epic Necessário",
		epicAuthRequiredMessage: "Por favor, entre em sua conta Epic Games nas configurações do Unifideck",
		installingRedist: "Instalando Redistribuíveis",
		installingRedistMessage: "Executando a instalação inicial de redistribuíveis do GOG...",
		setupFailed: "Configuração Falhou",
		setupFailedMessage: "A instalação falhou, veja os logs para detalhes",
		prerequisitesTitle: "Instalando Pré-requisitos",
		installingPrerequisites: "Instalando pré-requisitos necessários do jogo...",
		prerequisitesReady: "Pré-requisitos Prontos",
		prerequisitesInstalled: "Pré-requisitos do jogo instalados",
		prerequisitesFailed: "Aviso de Pré-requisitos",
		prerequisitesFailedMessage: "Pré-requisitos podem não ter sido instalados corretamente",
		browserRequired: "Microsoft Edge Necessário",
		signingIn: "Entrando...",
		cloudSyncDownFailed: "Falha no download do save na nuvem para {{store}} ({{error}}). Jogando com saves locais.",
		cloudSyncUpFailed: "Falha no upload do save na nuvem para {{store}} ({{error}}). Seu progresso está salvo apenas localmente.",
		errorCircuitBreakerOpen: "{{game_key}} falhou ao iniciar {{count}} vezes recentemente. Inicializações automáticas pausadas.",
		launcherError: "Não foi possível iniciar {{game_key}} ({{error_code}}). Verifique os logs para detalhes.",
		downloadingProton: "Baixando Proton — isso pode levar alguns minutos...",
		downloadingRuntime: "Baixando o Steam Runtime — a primeira execução pode levar alguns minutos...",
		installingUbisoft: "Instalando via Ubisoft Connect",
		installingUbisoftMessage: "Abrindo o Ubisoft Connect — inicie a instalação na janela do launcher.",
		protonReadyBody: "{{version}} está pronto para uso",
		protonReadyTitle: "Proton pronto",
		protonSwitchedTo: "Usando {{version}} agora",
		signingInUbisoft: "Entrando no Ubisoft Connect",
		signingInUbisoftMessage: "Abrindo o Ubisoft Connect — faça login lá e depois volte.",
		ubisoftPrefixNotReady: "O Ubisoft Connect não está pronto",
		ubisoftPrefixNotReadyMessage: "Não foi possível preparar o prefixo do Ubisoft Connect para este jogo. Tente instalar novamente; se continuar falhando, execute uma nova sincronização da biblioteca.",
		ubisoftAuthPrefixNotReady: "O Ubisoft Connect não está pronto",
		ubisoftAuthPrefixNotReadyMessage: "Não foi possível preparar o Ubisoft Connect para o login. Tente novamente em um instante.",
		ubisoftLaunchIdMissing: "Abrindo o Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "Não foi possível resolver o ID de inicialização deste jogo — abrindo o Ubisoft Connect para que você possa iniciá-lo pela sua biblioteca. Execute uma nova sincronização da biblioteca para corrigir a inicialização direta."
	},
	updateComplete: "Atualização Concluída!",
	updateCompleteMessage: "{{title}} foi atualizado.",
	updateCancelled: "Atualização Cancelada",
	updateCancelledMessage: "A atualização de {{title}} foi cancelada.",
	updateFailed: "Atualização Falhou",
	updateFailedMessage: "Falha ao iniciar atualização.",
	storeError: "Falha ao comunicar com {{store}} ({{errType}})",
	captureLogsStarted: "Coletando logs...",
	captureLogsStartedMessage: "Criando um pacote de diagnóstico na sua pasta Downloads. Isso leva alguns segundos.",
	captureLogsDone: "Logs capturados",
	captureLogsDoneMessage: "Salvo em {{path}} ({{size}}, {{files}} arquivos).",
	captureLogsPartial: "Logs capturados (parcial)",
	captureLogsPartialMessage: "Salvo em {{path}} ({{size}}, {{files}} arquivos). Não foi possível ler {{failures}} arquivo(s).",
	captureLogsFailed: "Não foi possível capturar os logs",
	captureLogsBusy: "Uma captura de logs já está em andamento",
	captureLogsCopied: "Caminho copiado",
	captureLogsCopyFailed: "Não foi possível copiar o caminho",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Liberar espaço",
		signInToStore: "Entrar",
		retrySync: "Tentar novamente",
		retryLibrarySync: "Atualizar biblioteca",
		openSaveFolder: "Mostrar pasta de saves",
		showLogs: "Mostrar logs"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Não foi possível atualizar a biblioteca {{store}} ({{error}}). Sua biblioteca existente continua visível."
	},
	cloudPullStarted: "Baixando salvamento na nuvem…",
	cloudPushStarted: "Enviando salvamento local…",
	cloudSavePathFailed: "Não foi possível definir o local de salvamento",
	cloudSavePathSet: "Local de salvamento atualizado",
	cloudPullDone: "Salvamento na nuvem baixado",
	cloudPullFailed: "Falha ao baixar salvamento na nuvem",
	cloudPushDone: "Salvamento local enviado",
	cloudPushFailed: "Falha ao enviar salvamento local"
};
var uninstallModal$a = {
	title: "Confirmar Desinstalação",
	description: "Tem certeza de que deseja desinstalar {{title}}?",
	deleteProtonLabel: "Também excluir arquivos do Proton",
	deleteProtonDescription: "Remove dados de compatibilidade e arquivos de usuário",
	warningTitle: "Aviso:",
	warningBody: "Isso excluirá permanentemente os dados de salvamento armazenados no prefixo do Wine. Certifique-se de que eles estão salvos.",
	cancel: "Cancelar",
	uninstall: "Desinstalar"
};
var languageSettings$a = {
	title: "IDIOMA",
	label: "Idioma de Exibição",
	description: "Idioma da interface e download dos jogos (quando suportado)",
	autoDetect: "Auto (Sistema)"
};
var errors$a = {
	legendaryNotFound: "Epic Games CLI (Legendary) não encontrado",
	gogdlNotFound: "GOG CLI (gogdl) não encontrado",
	nileNotFound: "Amazon Games CLI (Nile) não encontrado",
	authUrlFailed: "Não foi possível obter URL de autenticação",
	authFailed: "Falha na autenticação",
	notAuthenticated: "Não autenticado. Por favor, entre primeiro.",
	syncInProgress: "Uma sincronização já está em progresso",
	syncCancelled: "Sincronização cancelada pelo usuário",
	gameNotFound: "Jogo não encontrado",
	gameNotInstalled: "O jogo não está instalado",
	installFailed: "Falha na instalação — verifique os logs para detalhes",
	installNoDirectory: "Instalação concluída mas não foi possível localizar a pasta do jogo",
	uninstallFailed: "Falha ao desinstalar",
	lockConflict: "Conflito de bloqueio — por favor, tente novamente",
	unknownStore: "Loja desconhecida",
	shortcutWriteFailed: "Falha ao gravar atalhos",
	backgroundSyncDisabled: "Sincronização em segundo plano desativada",
	steamGridDbUnavailable: "Biblioteca do SteamGridDB indisponível",
	deleteSyncInProgress: "Não é possível excluir enquanto a sincronização está em progresso",
	alreadyInQueue: "Já está na fila",
	download: {
		diskSpace: "Espaço em disco insuficiente disponível",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Erro de rede — verifique sua conexão",
		authExpired: "Sessão expirada — por favor, entre novamente",
		toolMissing: "Ferramenta de download necessária não encontrada",
		processFailed: "Download falhou — por favor, tente novamente",
		directoryNotFound: "Não foi possível localizar os arquivos instalados",
		lockConflict: "Outra operação está em progresso — tente novamente",
		ubisoftClient: "Erro no cliente Ubisoft Connect — tente reparar o prefixo",
		ubisoftClientNotRunning: "O cliente Ubisoft Connect não está respondendo",
		ubisoftBootstrap: "Falha ao configurar o prefixo do Ubisoft Connect",
		ubisoftUmuMissing: "umu-run não encontrado — necessário para jogos da Ubisoft",
		generic: "Instalação falhou — verifique os logs para detalhes"
	},
	unknown: "Erro desconhecido",
	steamAppsUnavailable: "API SteamClient.Apps indisponível"
};
var sync$a = {
	start: "Sincronizar Bibliotecas",
	force: "Forçar Sincronização",
	syncing: "Sincronizando…",
	cancel: "Cancelar Sincronização",
	cancelling: "Cancelando…",
	preparing: "Preparando…",
	downloadingArtwork: "Baixando arte para {{game}}...",
	fetchingGameLists: "Buscando listas de jogos...",
	fetchingStore: "Buscando {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Sincronização cancelada pelo usuário",
	checkingInstalledGames: "Verificando jogos instalados...",
	sgdbLookup: "Procurando {{count}} jogos no SteamGridDB...",
	extractingSteamMetadata: "Extraindo metadados do Steam...",
	fetchingEnhancedMetadata: "Buscando metadados aprimorados...",
	lookingUpUnifiDB: "Buscando metadados do unifiDB...",
	fetchingMetacriticData: "Buscando metadados do Metacritic...",
	fetchingCompatData: "Verificando dados de compatibilidade...",
	checkingExistingArtwork: "Verificando artes existentes...",
	cleaningOrphanedArtwork: "Limpando artes órfãs...",
	retryingMissingArtwork: "Tentando novamente {{synced}} / {{total}} jogos com artes ausentes...",
	savingShortcuts: "Salvando atalhos...",
	completed: "Sincronização concluída!",
	extractingMetadata: "Extraindo metadados para {{game}}...",
	cooldown: "Aguarde {{secs}}s",
	artworkPhase: "Arte {{done}} / {{total}}"
};
var force_sync$a = {
	migratingOldInstallations: "Sincronização forçada: Migrando instalações antig...",
	fetchingGameLists: "Sincronização forçada: Buscando listas de jogos...",
	checkingInstalledGames: "Sincronização forçada: Verificando jogos instalados...",
	rewritingShortcuts: "Sincronização forçada: Reescrevendo atalhos...",
	cancelledShortcutsSaved: "Sincronização forçada cancelada - atalhos salvos",
	writingShortcuts: "Sincronização forçada: Gravando atalhos...",
	clearingProton: "Sincronização forçada: Limpando compatibilidade do Proton...",
	completed: "Sincronização forçada concluída! Atualizado {{updated}} jogos, buscadas {{artwork}} artes."
};
var artwork$a = {
	checking: "Verificando artes...",
	queueRefresh: "Enfileirando todos os jogos para atualização de artes...",
	downloadProgress: "Artes baixadas: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$a = {
	greatOnDeck: "Ótimos no Deck",
	allGames: "Todos os Jogos",
	installed: "Instalado",
	nonSteam: "Não Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$a = {
	error: "Erro do Unifideck",
	errorLoadingView: "Falha ao carregar visualização de biblioteca unificada",
	checkConsole: "Verifique o console do navegador para detalhes",
	errorLoadingGames: "Erro ao carregar jogos",
	tryReloading: "Tente recarregar o plugin ou verifique o console para detalhes",
	storeLabel: "Loja:",
	allStores: "Todas as Lojas",
	searchPlaceholder: "Pesquisar jogos...",
	library: "Biblioteca"
};
var gameInfoPanel$a = {
	loading: "Carregando informações do jogo...",
	noMetadata: "Nenhum metadado disponível para este jogo.",
	noTestResults: "Nenhum resultado de teste detalhado disponível.",
	buttons: {
		details: "Detalhes",
		synopsis: "Sinopse",
		storePage: "Página da Loja",
		dlc: "DLC",
		communityHub: "Central da Comunidade",
		pointsShop: "Loja de Pontos",
		discussions: "Discussões",
		guides: "Guias",
		support: "Suporte",
		install: "Instalar",
		uninstall: "Desinstalar",
		cancel: "Cancelar",
		achievements: "Conquistas"
	},
	labels: {
		store: "Loja:",
		name: "Nome:",
		size: "Tamanho:",
		developer: "Desenvolvedor:",
		publisher: "Distribuidora:",
		released: "Lançamento:",
		metacritic: "Metacritic:",
		status: "Status:",
		spaceRequired: "Espaço Necessário",
		lastPlayed: "Última vez jogado",
		playTime: "Tempo de jogo",
		never: "Nunca"
	},
	compatibility: {
		modalTitle: "Compatibilidade com Steam Deck",
		viewOnProtonDb: "Visualizar no ProtonDB",
		close: "Fechar",
		verified: "VERIFICADO",
		playable: "JOGÁVEL",
		unsupported: "NÃO COMPATÍVEL",
		unknown: "DESCONHECIDO"
	},
	failedToLoadMetadata: "Falha ao carregar metadados",
	lastSession: "Última sessão: desbloqueado {{names}}"
};
var gameDetailsSettings$a = {
	title: "VISUALIZAÇÃO DOS DETALHES DO JOGO",
	simple: "Simples",
	detailed: "Detalhado"
};
var gogLanguageModal$a = {
	title: "Selecionar Idioma",
	description: "Escolha qual idioma baixar para {{title}}",
	label: "Idioma",
	install: "Instalar",
	cancel: "Cancelar"
};
var accountSwitch$a = {
	title: "Mudança de conta detectada",
	description: "Você entrou com uma conta Steam diferente. O que você gostaria de fazer com seus dados do Unifideck?",
	migrate: "Migrar dados do Unifideck",
	migrateDescription: "Copiar jogos e artes da conta anterior",
	freshStart: "Começar do zero",
	freshStartDescription: "Limpar logins das lojas para conectar contas diferentes do Epic/GOG/Amazon/Ubisoft",
	skip: "Pular",
	toastMigrateTitle: "Migração concluída",
	toastMigrateBody: "Migrados {{shortcuts}} atalhos e {{artwork}} arquivos de artes",
	toastClearTitle: "Tokens de autenticação limpos",
	toastClearBody: "Logins das lojas foram limpos. Agora você pode entrar com contas diferentes."
};
var ubisoftAuth$a = {
	title: "Login do Ubisoft Connect",
	description: "Entre pelo Ubisoft Connect para vincular sua biblioteca.",
	signInViaUpc: "Entrar pelo Ubisoft Connect",
	upcDescription: "O Ubisoft Connect será aberto para você fazer login. Depois de entrar, sua sessão será capturada automaticamente.",
	launchingUpc: "Iniciando Ubisoft Connect...",
	waitingForLogin: "Aguardando login...",
	retryLaunch: "Tentar Novamente",
	cancel: "Cancelar",
	launchFailed: "Falha ao iniciar o Ubisoft Connect. Por favor, tente novamente.",
	signingIn: "Entrando...",
	emailLabel: "E-mail",
	passwordLabel: "Senha",
	signIn: "Entrar",
	twoFactorTitle: "Autenticação de dois fatores",
	twoFactorDescription: "Insira o código de verificação do seu aplicativo de autenticação.",
	twoFactorLabel: "Código de verificação",
	verify: "Verificar",
	verifying: "Verificando...",
	errorEmptyFields: "Por favor, insira seu e-mail e senha",
	errorEmpty2FA: "Por favor, insira o código de verificação",
	errorLoginFailed: "Falha no login. Por favor, verifique suas credenciais.",
	errorInvalid2FA: "Código de verificação inválido. Por favor, tente novamente.",
	showPassword: "Mostrar senha",
	hidePassword: "Ocultar senha",
	doubleLoginWarning: "Você precisará fazer login duas vezes para a Ubisoft."
};
var relativeDate$a = {
	today: "Hoje",
	yesterday: "Ontem"
};
var common$a = {
	thisGame: "este jogo",
	game: "Jogo",
	unknown: "Desconhecido",
	ok: "OK",
	cancel: "Cancelar",
	working: "Processando…",
	loading: "Carregando…",
	free: "grátis",
	close: "Fechar"
};
var info$a = {
	expandDetails: "Mostrar detalhes",
	collapseDetails: "Ocultar detalhes"
};
var microsoft$a = {
	notCompatible: "Este formato de jogo não é compatível com o SteamOS",
	playOnCloud: "Jogar na Nuvem",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Entre com sua conta Microsoft / Xbox",
	accountConnected: "Conta Microsoft conectada",
	loggedOut: "Sessão encerrada no Microsoft Store",
	xcloudNotInstalled: "Jogos do xCloud não são instalados localmente",
	chromiumRequired: "Microsoft Edge Necessário",
	chromiumRequiredMessage: "O login requer o Microsoft Edge. Deseja instalá-lo automaticamente? (Isso só precisa ser feito uma vez).",
	chromiumInstallButton: "Instalar Microsoft Edge",
	chromiumInstalling: "Instalando Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge instalado!",
	chromiumAlreadyInstalled: "Um navegador compatível já está instalado",
	chromiumInstallFailed: "Falha ao instalar o Microsoft Edge. Por favor, instale manualmente: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Tempo limite de instalação do Microsoft Edge excedido. Por favor, verifique sua conexão com a internet.",
	browserAlreadyInstalled: "Um navegador compatível já está instalado",
	browserInstalled: "Microsoft Edge instalado com sucesso!",
	browserInstallFailed: "Falha ao instalar o Microsoft Edge. Por favor, instale manualmente: flatpak install com.microsoft.Edge",
	flatpakNotFound: "O Flatpak não está disponível neste sistema",
	signInRequired: "Entre para jogar",
	syncSkippedNoSubscription: "Nenhuma assinatura ativa do Xbox Game Pass detectada. Sua biblioteca do xCloud não foi sincronizada.",
	syncSkippedTierUnknown: "Assinatura do Xbox Game Pass detectada, mas não foi possível determinar o plano. Sincronização pulada para evitar listar jogos indisponíveis.",
	subscriptionCheckFailed: "Não foi possível verificar sua assinatura do Xbox Game Pass. Sincronização do xCloud pulada. Tente novamente quando sua conexão estabilizar.",
	subscriptionDetected: "Assinatura ativa do Xbox Game Pass {{tier}}."
};
var playButton$a = {
	play: "Jogar",
	install: "Instalar",
	cancel: "Cancelar",
	retry: "Tentar novamente",
	downloadFailed: "Falha no download",
	installing: "Instalando",
	controllerConfig: "Configuração do Controle",
	appSettings: "Configurações do App"
};
var play$a = {
	play: "Jogar",
	resume: "Retomar",
	stop: "Parar",
	cancel: "Cancelar",
	cancelling: "Cancelando…",
	update: "Atualizar",
	uninstall: "Desinstalar",
	queued: "Na fila",
	downloading: "Baixando",
	extracting: "Extraindo",
	verifying: "Verificando",
	finalizing: "Finalizando instalação",
	cancelConfirmTitle: "Cancelar download?",
	cancelConfirmBody: "Tem certeza de que deseja cancelar o download de {{title}}? Os arquivos parciais serão descartados.",
	cancelConfirmConfirm: "Cancelar download",
	cancelConfirmCancel: "Continuar baixando",
	playOnCloud: "Jogar na Nuvem",
	cloudSave: {
		autoPullOn: "Os salvamentos na nuvem são baixados automaticamente na inicialização.",
		close: "Fechar",
		download: "Baixar salvamento na nuvem",
		filesCount: "{{count}} arquivos",
		label: "Salvamentos na nuvem",
		menuTitle: "Salvamentos na nuvem — {{game}}",
		noCloudData: "Desconhecido",
		noCloudSupport: "Sem suporte a salvamento na nuvem",
		setLocation: "Definir local de salvamento",
		setLocationHint: "Escolha a pasta onde este jogo armazena seus salvamentos.",
		setLocationTitle: "Escolha a pasta de salvamento deste jogo",
		statusCloudAvailable: "Salvamento na nuvem disponível",
		statusNoSaves: "Sem salvamentos locais",
		statusSyncing: "Sincronizando salvamentos na nuvem…",
		statusUnresolved: "Local de salvamento não encontrado — a sincronização pode não funcionar para este jogo.",
		unresolvedHint: "Inicie o jogo uma vez para que possamos detectar sua pasta de salvamento ou defina o local manualmente.",
		upload: "Enviar salvamento local"
	},
	exe: {
		browse: "Procurar…",
		changeFailed: "Não foi possível alterar o executável",
		changed: "Executável alterado",
		defaultTag: "(padrão)",
		menuItem: "Alterar executável…",
		reset: "Redefinir para o executável padrão",
		resetButton: "Redefinir para o padrão",
		subtitle: "Escolha qual arquivo este jogo inicia (por exemplo, pular um inicializador e executar o jogo diretamente). O diretório de trabalho e os salvamentos na nuvem não serão afetados.",
		title: "Alterar executável — {{game}}",
		unavailable: "Não foi possível ler a pasta deste jogo."
	}
};
var playMeta$a = {
	spaceRequired: "Espaço necessário",
	installedSize: "Tamanho instalado",
	lastPlayed: "Última vez jogado",
	neverPlayed: "Nunca jogado",
	hoursShort: "h",
	minutesShort: "m",
	played: "Jogados"
};
var downloads$a = {
	title: "Downloads",
	empty: "Nenhum download. Instale jogos da sua biblioteca para vê-los aqui.",
	current: "Baixando agora",
	queued: "Na fila",
	finished: "Concluído recentemente",
	cancel: "Cancelar",
	removeFromQueue: "Remover",
	play: "Jogar",
	outcome: {
		queued: "Na fila",
		downloading: "Baixando",
		completed: "Concluído",
		cancelled: "Cancelado",
		error: "Falhou"
	}
};
var navigation$a = {
	loading: "Carregando...",
	storesTitle: "Lojas",
	syncTitle: "Sinc. de biblioteca",
	languageTitle: "Idioma"
};
var security$a = {
	panelTitle: "Segurança",
	counters: {
		label: "Contadores",
		empty: "Nenhum evento observado desde o início"
	},
	log: {
		label: "Registro de auditoria",
		entryCount: "{{count}} entrada",
		entryCount_other: "{{count}} entradas",
		emptyFiltered: "Nenhum evento corresponde ao filtro"
	},
	filter: {
		label: "Filtro",
		all: "Todos os eventos"
	},
	timeline: {
		label: "Linha do tempo de 24h ({{count}} evento)",
		label_other: "Linha do tempo de 24h ({{count}} eventos)",
		axisStart: "-24h",
		axisEnd: "agora"
	},
	relativeTime: {
		seconds: "{{count}}s atrás",
		minutes: "{{count}} min atrás",
		hours: "{{count}}h atrás",
		days: "{{count}}d atrás"
	},
	bruteforce: {
		warningTitle: "Aviso: detecção de força bruta (nível de aviso)",
		escalationTitle: "ALERTA: detecção de força bruta (nível de escalada)",
		detail: "{{failures}} falhas de descriptografia nos últimos {{window}}s. Limites: {{warning}} (aviso) / {{escalation}} (escalada).",
		resetButton: "Confirmar e redefinir o detector"
	},
	actions: {
		clearLog: "Limpar registro e contadores"
	},
	events: {
		tokenEncrypted: "Token criptografado",
		tokenDecrypted: "Token descriptografado",
		decryptFailed: "Falha ao descriptografar",
		tokenFileMigrated: "Arquivo de token migrado",
		legacyPlaintextDetected: "Texto simples antigo detectado",
		authFlowStarted: "Autenticação iniciada",
		authFlowCompleted: "Autenticação concluída",
		authFlowFailed: "Autenticação falhou",
		permissionsCheck: "Permissões verificadas",
		permissionsRepaired: "Permissões reparadas",
		bruteforceSuspected: "Suspeita de força bruta",
		deviceResetDetected: "Redefinição de dispositivo detectada",
		fingerprintInitialized: "Digital inicializada",
		externalAuthCheckFailed: "Falha na verificação de autenticação externa",
		configValidationCompleted: "Validação de configuração concluída",
		configValidationFailed: "Validação de configuração falhou"
	},
	configValidation: {
		degradedTitle: "Configuração inválida: modo degradado ativo",
		detail: "{{count}} erro de validação detectado ao iniciar",
		detail_other: "{{count}} erros de validação detectados ao iniciar",
		sourceDefaults: "valores padrão",
		sourceUser: "configurações do usuário",
		sourceMerged: "mesclado",
		overflow: "+ {{count}} outro erro",
		overflow_other: "+ {{count}} outros erros"
	}
};
var cloudSyncBehavior$a = {
	_comment: "Painel de configurações que permite ao usuário escolher, por loja, como os erros de sincronização de save na nuvem devem ser exibidos. As cinco entradas de store_label refletem as linhas renderizadas pelo CloudSyncBehaviorPanel.tsx. A Microsoft é deliberadamente omitida: o streaming do xCloud não tem save local para sincronizar.",
	title: "Comportamento de sinc. de save na nuvem",
	description: "Escolha como o Unifideck deve reagire quando o download ou upload de save na nuvem falhar ao iniciar um jogo.",
	loadError: "Não foi possível carregar as configurações atuais",
	saveError: "Não foi possível salvar sua alteração. Por favor, tente novamente",
	store_label: {
		"default": "Padrão (todas as lojas)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Silencioso (apenas registro)",
		toast: "Notificar (mensagem emergente)"
	}
};
var cloudSync$a = {
	_comment: "Mensagens pop-up emitidas pelo launcher quando a sincronização na nuvem falha. O espaço reservado do erro é resolvido via cloudSync.error.<code> por meio do campo error_i18n_key do payload del backend.",
	error: {
		network_unreachable: "Rede indisponível",
		auth_expired: "Autenticação expirada",
		forbidden: "Acesso negado",
		quota_exceeded: "Cota de armazenamento na nuvem excedida",
		server_error: "Servidor da loja indisponível",
		timed_out: "Tempo limite da solicitação expirado",
		disk_full: "Espaço em disco insuficiente",
		disk_space_low: "Pulado: espaço em disco baixo (libere espaço e tente novamente)",
		permission_denied: "Permissão negada",
		cancelled: "Cancelado",
		unknown: "Erro inesperado"
	},
	unavailableNativeDep: "Os salvamentos na nuvem estão desativados neste sistema — o Python ({{python}}) dele não possui um componente necessário. Seus salvamentos locais não são afetados; reconstrua/reinstale o plugin após uma atualização do sistema para restaurar a sincronização."
};
var launchLogs$a = {
	_comment: "Mensagens para o LaunchLogsModal. O modal é aberto a partir da ação de notificação unifideck://show-logs/<launch_id>, anexada aos avisos de falha do launcher (circuit breaker + LauncherError). Lê os logs arquivados para cada inicialização em ~/.local/share/unifideck/launches/<launch_id>.log e os exibe coloridos por gravidade.",
	title: "Logs de inicialização — {{launchId}}",
	close: "Fechar",
	copyAll: "Copiar tudo",
	"export": "Exportar para arquivo",
	copied: "Copiado para a área de transferência",
	copyFailed: "Falha ao copiar",
	exporting: "Exportando…",
	exportedTo: "Exportado para {{path}}",
	exportFailed: "Erro de exportação: {{error}}",
	loading: "Carregando logs…",
	notAvailable: "Os logs desta inicialização não estão mais disponíveis (além do período de retenção de 7 dias).",
	error: "Não foi possível ler os logs: {{error}}",
	fileInfo: "{{path}} — exibindo {{shown}} de {{total}} linhas",
	filterLabel: "Exibir",
	filterInfo: "Informações",
	filterWarning: "Aviso",
	filterError: "Erro",
	nextError: "Próximo erro ({{current}}/{{total}})",
	noErrors: "Sem erros",
	allFiltered: "Todas as linhas ocultas pelo filtro atual. Ative pelo menos um nível de gravidade para ver os registros."
};
var settings$a = {
	_comment: "Controles de ação do UnifideckSettingsPanel que não estão cobertos por componentes mais específicos (LanguageSelector, CloudSyncBehaviorPanel). refreshAll é um botão que aciona sync_all() em todas as lojas registradas; a visualização da biblioteca exibe o progresso da sincronização por meio de sua própria inscrição nos eventos SYNC_PROGRESS.",
	storeConnections: "Conexões de lojas",
	librarySync: "Sinc. de biblioteca",
	language: "Idioma",
	languageAuto: "Detectar automaticamente",
	refreshAll: {
		sectionTitle: "Ações da biblioteca",
		button: "Atualizar todas as bibliotecas",
		buttonBusy: "Iniciando…",
		startedTitle: "Atualização de biblioteca iniciada",
		startedBody: "Sincronizando todas as lojas em segundo plano.",
		startedSingle: "Atualizando a biblioteca da {{store}} em segundo plano.",
		failedTitle: "Não foi possível iniciar a atualização da biblioteca"
	}
};
var saveFolder$a = {
	_comment: "Mensagens para o SaveFolderModal. O modal é aberto a partir da ação de notificação unifideck://open-save-folder/<store>/<game_id>, normalmente exibida em falhas de sincronização na nuvem quando o usuário deseja inspecionar o que está salvo localmente. count e size são interpolados; truncated é exibido apenas se o resultado foi limitado.",
	title: "Pasta de saves — {{store}} / {{gameId}}",
	close: "Fechar",
	copyPath: "Copiar caminho",
	copyFileList: "Copiar lista de arquivos",
	depthLabel: "Profundidade",
	filterLabel: "Filtro",
	loading: "Carregando conteúdo da pasta…",
	noSavesYet: "Nenhum save local encontrado para este jogo ainda. A pasta será criada na próxima sincronização na nuvem bem-sucedida.",
	aggregate: "{{count}} arquivos, {{size}} no total",
	truncated: "Outros {{count}} arquivos ({{size}}) não exibidos. Reduza seu filtro para visualizá-los.",
	error: "Não foi possível ler a pasta de saves: {{error}}"
};
var library$a = {
	_comment: "Serviços de interface de usuário por jogo na visualização da biblioteca, incluindo o indicador do disjuntor de inicialização e controles de inicialização forçada / redefinição quando as inicializações falham repetidamente.",
	circuitBreaker: {
		_comment: "UI para o disjuntor de falhas de inicialização. O indicador aparece apenas quando as falhas na janela configurada atingem circuit_breaker.failures_threshold (padrão 3 em 600s). A inicialização forçada aciona um bypass de uso único; Redefinir limpa todo o histórico de falhas.",
		badgeRecent: "{{count}} falha de inicialização recente",
		badgeRecent_other: "{{count}} falhas de inicialização recentes",
		badgeTooltip: "{{count}} inicializações com falha na janela temporal recente. O disjuntor pausou as inicializações automáticas.",
		forceLaunchButton: "Forçar inicialização",
		resetButton: "Redefinir falhas",
		failureKind: {
			fastBoot: "Travamento na inicialização rápida",
			launcherError: "Erro de preparação do launcher"
		}
	}
};
var pickStorage$a = {
	title: "Local de Instalação",
	customOption: "Escolher outro local...",
	customLabel: "Local personalizado",
	change: "Alterar...",
	gameSize: "Tamanho da instalação"
};
var restart$a = {
	title: "Sincronização Concluída!",
	body: "Por favor, reinicie o Steam para aplicar seus novos jogos e artes",
	confirm: "Reiniciar Agora",
	later: "Mais Tarde"
};
var updater$a = {
	checkButton: "Verificar Atualizações",
	checkCompleteMessage: "Informações da última versão obtidas com sucesso.",
	checkCompleteTitle: "Verificação de Atualização Concluída",
	checkFailedTitle: "Verificação Falhou",
	checkingButton: "Verificando...",
	currentTitle: "Atual",
	downgradeButton: "Downgrade para v{{version}}",
	installFailedTitle: "Instalação Falhou",
	installTimeoutMessage: "Sem resposta do Decky Loader — a instalação pode ter travado ou falhado. Tente novamente.",
	installedLabel: "instalado",
	installingButton: "Instalando...",
	installingTitle: "Instalando Plugin",
	latestLabel: "mais recente",
	modalTitle: "UNIFIDECK v{{version}} — Notas de Versão",
	noBackend: "O backend do Decky não está disponível.",
	noReleaseNotes: "Nenhuma nota de versão disponível.",
	reinstallButton: "Reinstalar v{{version}}",
	refreshFailedMessage: "Não foi possível verificar as informações da última versão. Verifique sua conexão e tente novamente.",
	releaseGoneMessage: "Esta versão não está mais disponível. Verifique as atualizações e selecione novamente.",
	releaseNotesButton: "Notas de Versão",
	stageDownload: "Baixando…",
	stageFinish: "Finalizando…",
	stageInstall: "Instalando…",
	stageOpen: "Lendo pacote…",
	stageParse: "Verificando…",
	stageRemove: "Removendo versão antiga…",
	stageStart: "Iniciando…",
	stageWorking: "Instalando…",
	titleLoading: "Verificando versão",
	typeDowngrade: "Downgrade para",
	typeReinstall: "Reinstalando",
	typeUpdate: "Atualizando para",
	updateButton: "Atualizar para v{{version}}"
};
var achievements$a = {
	unlockedTitle: "Conquista desbloqueada",
	unlockedBody: "{{name}}"
};
var achievementsModal$a = {
	title: "Conquistas",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Atualizar",
	close: "Fechar",
	loading: "Carregando conquistas…",
	empty: "Este jogo não tem conquistas.",
	hidden: "Conquista oculta",
	unlockedOn: "Desbloqueado em {{date}}",
	error: "Não foi possível carregar as conquistas.",
	errorOffline: "Conecte-se à internet para ver as conquistas.",
	errorAuth: "Faça login na loja deste jogo para ver as conquistas.",
	errorUnavailable: "As conquistas não estão disponíveis para este jogo."
};
var collectionSettings$a = {
	title: "COLEÇÕES DO STEAM",
	enabled: "Coleções ativadas",
	disabled: "Coleções desativadas"
};
var ptBR = {
	_comment: _comment$a,
	tabs: tabs$a,
	auth: auth$a,
	storage: storage$a,
	storageSettings: storageSettings$a,
	gameGrid: gameGrid$a,
	downloadsTab: downloadsTab$a,
	cloudSave: cloudSave$a,
	storeConnections: storeConnections$a,
	librarySync: librarySync$a,
	cleanup: cleanup$a,
	captureLogs: captureLogs$a,
	installButton: installButton$a,
	confirmModals: confirmModals$a,
	toasts: toasts$a,
	uninstallModal: uninstallModal$a,
	languageSettings: languageSettings$a,
	errors: errors$a,
	sync: sync$a,
	force_sync: force_sync$a,
	artwork: artwork$a,
	deckTabs: deckTabs$a,
	unifiedLibrary: unifiedLibrary$a,
	gameInfoPanel: gameInfoPanel$a,
	gameDetailsSettings: gameDetailsSettings$a,
	gogLanguageModal: gogLanguageModal$a,
	accountSwitch: accountSwitch$a,
	ubisoftAuth: ubisoftAuth$a,
	relativeDate: relativeDate$a,
	common: common$a,
	info: info$a,
	microsoft: microsoft$a,
	playButton: playButton$a,
	play: play$a,
	playMeta: playMeta$a,
	downloads: downloads$a,
	navigation: navigation$a,
	security: security$a,
	cloudSyncBehavior: cloudSyncBehavior$a,
	cloudSync: cloudSync$a,
	launchLogs: launchLogs$a,
	settings: settings$a,
	saveFolder: saveFolder$a,
	library: library$a,
	pickStorage: pickStorage$a,
	restart: restart$a,
	updater: updater$a,
	achievements: achievements$a,
	achievementsModal: achievementsModal$a,
	collectionSettings: collectionSettings$a
};

var _comment$9 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$9 = {
	settings: "Instellingen",
	downloads: "Downloads"
};
var auth$9 = {
	connect: "Verbinden",
	disconnect: "Verbinding verbreken",
	status: {
		connected: "Verbonden",
		disconnected: "Niet verbonden",
		checking: "Controleren…",
		error: "Fout — controleer logs"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$9 = {
	title: "Opslag",
	defaultUpdated: "Standaard opslag bijgewerkt",
	isDefault: "Standaard",
	setDefault: "Als standaard instellen",
	customPath: "Aangepast pad",
	up: "↑ Omhoog",
	refresh: "Vernieuwen",
	useThisPath: "Gebruik dit pad"
};
var storageSettings$9 = {
	title: "Downloadinstellingen",
	installLocationLabel: "Installatielocatie",
	installLocationDescription: "Waar nieuwe games worden gedownload",
	pathLabel: "Pad",
	loading: "Opslagopties laden...",
	toastUpdatedTitle: "Opslaglocatie bijgewerkt",
	toastUpdatedBody: "Nieuwe games worden geïnstalleerd in {{location}}",
	toastFailedTitle: "Bijwerken mislukt",
	toastFailedBody: "{{error}}",
	unknown: "Onbekend",
	browseButton: "Bladeren...",
	clearCustom: "Aangepaste locatie wissen",
	customPathSet: "Aangepaste installatielocatie ingesteld",
	newFolder: "Nieuwe map",
	newFolderCreate: "Maken",
	selectFolder: "Gebruik deze map",
	emptyDirectory: "Lege map",
	showHidden: "Verborgen weergeven",
	selectDevice: "Selecteer apparaat",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Gewijzigd (nieuwste)",
	sortModifiedOldest: "Gewijzigd (oudste)",
	sortCreatedNewest: "Gemaakt (nieuwste)",
	sortCreatedOldest: "Gemaakt (oudste)",
	sortSizeLargest: "Grootte (grootste)",
	sortSizeSmallest: "Grootte (kleinste)",
	"validationErrors.notAbsolute": "Pad moet absoluut zijn",
	"validationErrors.systemDir": "Kan geen systeemmapp gebruiken",
	"validationErrors.notWritable": "Locatie is niet schrijfbaar",
	"validationErrors.notFound": "Map bestaat niet",
	"validationErrors.homeRoot": "Kan de root van de gebruikersmap niet gebruiken",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB vrij)"
};
var gameGrid$9 = {
	loading: "Games laden...",
	noGamesFound: "Geen games gevonden",
	noGamesHint: "Probeer je bibliotheken te synchroniseren via de Unifideck-instellingen",
	showingGames: "Weergave van {{count}} games",
	installed: "Geïnstalleerd",
	notInstalled: "Niet geïnstalleerd",
	playtime: "{{hours}}u {{minutes}}m gespeeld"
};
var downloadsTab$9 = {
	currentDownload: "HUIDIGE DOWNLOAD",
	queuedDownloads: "IN WACHTRIJ ({{count}})",
	recentlyCompleted: "ONLANGS VOLTOOID",
	noActiveDownloads: "Geen actieve downloads",
	noDownloads: "Geen downloads. Installeer games uit je bibliotheek om ze hier te zien.",
	loadingLabel: "Laden...",
	loadingMessage: "Downloadwachtrij ophalen...",
	confirmCancelTitle: "Annulering bevestigen",
	confirmCancelDescription: "Weet je zeker dat je de download van {{game}} wilt annuleren?",
	confirmCancelYes: "Ja",
	confirmCancelNo: "Nee",
	cancelDownload: "Annuleren",
	toastDownloadCancelledTitle: "Download geannuleerd",
	toastDownloadCancelledBody: "De download is verwijderd uit de wachtrij.",
	toastCancelFailedTitle: "Annuleren mislukt",
	toastCancelFailedBody: "Kan download niet annuleren: {{error}}",
	status: {
		queued: "In wachtrij",
		downloading: "Downloaden",
		completed: "Voltooid",
		cancelled: "Geannuleerd",
		error: "Fout"
	},
	preparingDownload: "Download voorbereiden...",
	phaseExtracting: "{{game}} uitpakken...",
	phaseVerifying: "{{game}} verifiëren...",
	internalStorage: "Interne opslag ({{freeSpace}} GB vrij)",
	sdCard: "SD-kaart ({{freeSpace}} GB vrij)",
	customLocation: "Aangepast: {{path}} ({{freeSpace}} GB vrij)",
	extractingLabel: "Uitpakken...",
	verifyingLabel: "Verifiëren...",
	updateQueuedLabel: "Update in wachtrij",
	downloadQueuedLabel: "Download in wachtrij",
	downloadingUpdateLabel: "Update downloaden",
	downloadingLabel: "Downloaden...",
	finalizingInstallation: "Installatie afronden...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "Resterende tijd: {{eta}}",
	preparingMessage: "Prefix initialiseren en cloudopslag synchroniseren...",
	preparingLabel: "Spel instellen...",
	installingViaUpcLabel: "Installeren via Ubisoft Connect",
	extractingMessage: "Bestanden uitpakken...",
	verifyingMessage: "Bestanden controleren... {{pct}}%",
	installingViaUpcMessage: "Volg het Ubisoft Connect-venster om de installatie te voltooien..."
};
var cloudSave$9 = {
	title: "Cloudopslagconflict",
	description: "Je lokale spelbestanden verschillen van de cloud voor {{game}}",
	local: "Lokaal:",
	cloud: "Cloud:",
	newer: "(nieuwer)",
	useCloud: "Gebruik cloud",
	useLocal: "Gebruik lokaal",
	unknownDate: "Onbekend",
	toastUsingCloudTitle: "Cloudopslag gebruiken",
	toastUsingCloudBody: "Cloudopslag voor {{game}} downloaden",
	toastUsingLocalTitle: "Lokale opslag gebruiken",
	toastUsingLocalBody: "Lokale opslag voor {{game}} uploaden",
	toastErrorTitle: "Fout",
	toastErrorBody: "Conflict oplossen mislukt",
	filesSize: "{{count}} bestanden · {{size}}",
	uploadBlockedEmpty: "Cloud-upload overgeslagen voor {{game}} — geen lokale saves gevonden",
	uploadSkippedTitle: "Cloud-upload overgeslagen",
	uploadSkippedBody: "Nog geen lokale saves om te uploaden voor {{game}}."
};
var storeConnections$9 = {
	title: "WINKELVERBINDINGEN",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Verbonden",
	checking: "Controleren...",
	authenticate: "Aanmelden",
	legendaryNotInstalled: "⚠️ Epic Games CLI installeren...",
	nileNotInstalled: "⚠️ Amazon Games CLI niet gevonden",
	error: "Fout - Controleer logs"
};
var librarySync$9 = {
	title: "BIBLIOTHEEK SYNC",
	syncLibraries: "Bibliotheken synchroniseren",
	forceSync: "Synchronisatie forceren",
	syncing: "Synchroniseren...",
	cancelSync: "Synchronisatie annuleren",
	retryStatusCheck: "Statuscontrole opnieuw proberen",
	gamesSynced: "{{synced}} / {{total}} games gesynchroniseerd",
	artworkDownloaded: "{{synced}} / {{total}} covers gedownload",
	metadataExtracted: "{{synced}} / {{total}} metadatarecords uitgepakt",
	steamMetadataDownloaded: "Steam-metadata: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDB-metadata: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacritic-data: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWG-metadata: {{synced}} / {{total}}",
	compatChecked: "Compatibiliteitsdata: {{synced}} / {{total}}",
	cancelling: "Annuleren…",
	error: "Fout"
};
var cleanup$9 = {
	title: "Opruimen",
	deleteAll: "Verwijder alle UNIFIDECK-bibliotheken en cache",
	modalTitle: "Alle Unifideck-data verwijderen?",
	modalDescription: "Dit verwijdert alle Unifideck-snelkoppelingen, Steam-artwork, opgeslagen winkellogins en gecachte gegevens. Geïnstalleerde games blijven op de schijf staan en kunnen opnieuw worden gesynchroniseerd. Schakel hieronder destructieve modus in om ook de geïnstalleerde gamebestanden te verwijderen.",
	deleteFilesLabel: "Ook geïnstalleerde gamebestanden verwijderen (Destructief)",
	deleteFilesDescription: "Verwijdert de installatiemap op de schijf voor elke Unifideck-game onder een veilige hoofdmap.",
	confirmDelete: "Ja, alles verwijderen",
	cancel: "Annuleren",
	deleting: "Verwijderen..."
};
var captureLogs$9 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (nl-NL).",
	title: "Logboeken vastleggen",
	description: "Verzamelt alle Unifideck-logboeken in één zipbestand in je map Downloads, zodat je het aan een bugrapport kunt toevoegen.",
	capture: "Logboeken vastleggen",
	capturing: "Logboeken verzamelen...",
	savedTo: "Opgeslagen in je map Downloads",
	bundleInfo: "{{files}} bestanden · {{size}}",
	partialNote: "{{count}} bestand kon niet worden gelezen. Het pakket is nog steeds bruikbaar.",
	partialNote_other: "{{count}} bestanden konden niet worden gelezen. Het pakket is nog steeds bruikbaar.",
	copyPath: "Pad kopiëren",
	modalTitle: "Diagnosepakket maken?",
	modalDescription: "Unifideck verzamelt zijn logboeken in één zipbestand in je map Downloads, zodat je het aan een bugrapport kunt toevoegen.",
	modalIncludes: "Inbegrepen: logboeken van de plug-in en van het starten van games, synchronisatie- en installatiegeschiedenis, je gametitels en installatiepaden, en gegevens over je apparaat zoals model, besturingssysteem en opslag.",
	modalExcludes: "Nooit inbegrepen: je wachtwoorden, inlogtokens van winkels of browsercookies.",
	modalConfirm: "Pakket maken",
	cancel: "Annuleren"
};
var installButton$9 = {
	play: "Spelen",
	resume: "Hervatten",
	install: "Installeer {{title}}",
	installNative: "Installeren",
	uninstall: "Deïnstalleer {{title}}",
	cancel: "✖ Annuleren",
	processing: "Verwerken...",
	checking: "Controleren...",
	size: "{{size}} GB",
	update: "UPDATE",
	updateAvailable: "UPDATE BESCHIKBAAR",
	updateAvailableDescription: "Een nieuwe versie is klaar om te installeren."
};
var confirmModals$9 = {
	installTitle: "Installatie bevestigen",
	installDescription: "Weet je zeker dat je {{title}} wilt installeren?",
	uninstallTitle: "Deïnstallatie bevestigen",
	uninstallDescription: "Weet je zeker dat je {{title}} wilt deïnstalleren?",
	cancelTitle: "Annulering bevestigen",
	cancelDescription: "Weet je zeker dat je de download van {{title}} wilt annuleren?",
	forceSyncTitle: "Synchronisatie forceren instellingen",
	forceSyncDescription: "Wil je alle artwork opnieuw synchroniseren? Dit zal handmatige wijzigingen in het artwork overschrijven. Selecteer 'Artwork behouden' om alleen ontbrekend artwork te downloaden.",
	resyncArtwork: "Artwork opnieuw synchroniseren",
	keepArtwork: "Artwork behouden",
	steamRestartTitle: "Synchronisatie voltooid!",
	steamRestartDescription: "Start Steam opnieuw op om je nieuwe games en artwork toe te passen",
	steamRestartDescriptionStore: "Start Steam opnieuw op om je nieuwe {{store}}-games and artwork toe te passen",
	steamRestartCleanupTitle: "Opruimen voltooid!",
	steamRestartCleanupDescription: "Start Steam opnieuw op om verwijderde games and artwork volledig te wissen",
	restartNow: "Nu herstarten",
	later: "Later",
	ubisoftInstallTitle: "Ubisoft Connect vereist",
	ubisoftInstallDescription: "Dit laadt de Ubisoft Connect-launcher die je nodig hebt om {{title}} te installeren.",
	ubisoftInstallConfirm: "Open Ubisoft Connect",
	yes: "Ja",
	no: "Nee",
	amazonUpdateTitle: "Volledig opnieuw downloaden vereist",
	amazonUpdateDescription: "Amazon-updates vereisen een volledige herdownload van {{title}}. Dit kan even duren. Doorgaan?",
	updateConfirm: "Bijwerken"
};
var toasts$9 = {
	downloadStarted: "Download gestart. Controleer UNIFIDECK > Downloads",
	downloadQueued: "Download in wachtrij",
	downloadQueuedBody: "{{title}} is toegevoegd aan de wachtrij.",
	downloadFailedUnknown: "Onbekende fout.",
	updateQueued: "Update in wachtrij",
	uninstallDone: "Deïnstallatie voltooid.",
	ubisoftLauncherOpening: "Ubisoft Connect openen",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect wordt geopend voor {{title}}. Installeer het spel daar en keer terug om te spelen.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect is al geopend voor deze installatie.",
	ubisoftLauncherOpenFailed: "Kan Ubisoft Connect niet openen",
	ubisoftLauncherOpenFailedMessage: "Kan Ubisoft Connect niet openen voor installatie.",
	multipartDetected: "Meerdelige download gedetecteerd",
	multipartMessage: "Wees geduldig en wacht tot de download is voltooid",
	downloadFailed: "Download mislukt",
	downloadFailedMessage: "Kan download niet starten.",
	downloadCancelled: "Download geannuleerd",
	downloadCancelledMessage: "Download van {{title}} geannuleerd.",
	cancelFailed: "Annuleren mislukt",
	cancelFailedMessage: "Kan download niet annuleren.",
	uninstalling: "Deïnstalleren...",
	uninstallingMessage: "Deïnstalleren van {{title}}...",
	uninstallingMessageProton: "Deïnstalleren van {{title}} en Proton-bestanden...",
	uninstallComplete: "Deïnstallatie voltooid",
	uninstallCompleteMessage: "{{title}} verwijderd.",
	uninstallCompleteMessageProton: "{{title}} verwijderd (inclusief Proton-bestanden).",
	installComplete: "Installatie voltooid!",
	installCompleteMessage: "{{title}} is klaar om te spelen.",
	syncComplete: "Synchronisatie voltooid!",
	syncCompleteMessage: "{{count}} games toegevoegd. HERSTART STEAM (sluit volledig af, keer niet alleen terug naar gamemodus) om ze in je bibliotheek te zien.",
	forceSyncComplete: "Geforceerde synchronisatie voltooid!",
	forceSyncCompleteMessage: "{{count}} games bijgewerkt. HERSTART STEAM om wijzigingen te zien.",
	syncCancelled: "Synchronisatie geannuleerd",
	authTimeout: "Authenticatie-time-out",
	authTimeoutMessage: "Authenticatie voor {{store}} is verlopen. Probeer het opnieuw.",
	authFailed: "Authenticatie mislukt",
	authFailedMessage: "Kan authenticatie niet starten",
	authError: "Authenticatiefout",
	authConnected: "{{store}} verbonden!",
	authConnectedMessage: "Succesvol geauthenticeerd bij {{store}}",
	cleanupStarted: "Opruimen gestart",
	cleanupStartedMessage: "Snelkoppelingen, artwork en opgeslagen winkellogins worden verwijderd. Dit kan even duren…",
	cleanupSuccessful: "Opruimen succesvol",
	cleanupSuccessfulMessage: "{{games}} snelkoppelingen, {{artwork}} artworkbestanden verwijderd, uitgelogd bij {{stores}} winkels, {{files}} installatiemappen verwijderd. Cache gewist.",
	cleanupNoop: "Niets op te ruimen",
	cleanupNoopMessage: "Unifideck heeft geen snelkoppelingen, artwork of opgeslagen logins om te verwijderen.",
	deleteFailed: "Verwijderen mislukt",
	repairPrefixSuccess: "Prefix gerepareerd",
	repairPrefixSuccessMessage: "Ubisoft Connect prefix is opnieuw opgebouwd",
	repairPrefixFailed: "Reparatie mislukt",
	repairPrefixFailedMessage: "Kan prefix niet repareren: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Netwerkfout",
		launchRetry: "Opnieuw starten",
		retryingUmu: "UMU opnieuw proberen in {{seconds}}s (poging {{attempt}}/{{max}})...",
		protonUpgrade: "Proton-upgrade",
		resettingPrefix: "Prefix resetten voor {{version}}...",
		firstTimeSetup: "Eerste installatie",
		initializingPrefix: "Wine-prefix initialiseren...",
		setupCompleteTitle: "Installatie voltooid",
		prefixInitialized: "Wine-prefix geïnitialiseerd",
		setupFallback: "Installatie",
		fallbackInitialization: "Fallback-initialisatie...",
		launchingGame: "Game starten",
		startingEpicGame: "Epic-game starten...",
		startingAmazonGame: "Amazon-game starten...",
		startingGogGame: "GOG-game starten...",
		startingUbisoftGame: "Ubisoft-game starten...",
		dependenciesTitle: "Afhankelijkheden installeren",
		installingRedistributables: "Redistributables installeren...",
		dependenciesReady: "Afhankelijkheden gereed",
		redistributablesInstalled: "Redistributables geïnstalleerd",
		dependenciesStatus: "Afhankelijkheden",
		checkLogs: "Installatie voltooid (controleer logs voor details)",
		installingProton: "Proton installeren",
		attemptingInstall: "Poging om {{version}} te installeren...",
		epicAuthRequired: "Epic Games-login vereist",
		epicAuthRequiredMessage: "Meld je aan bij Epic Games in de Unifideck-instellingen",
		installingRedist: "Redistributables installeren",
		installingRedistMessage: "Eerste installatie van GOG-redistributables uitvoeren...",
		setupFailed: "Installatie mislukt",
		setupFailedMessage: "Installatie mislukt, zie logs voor details",
		prerequisitesTitle: "Vereisten installeren",
		installingPrerequisites: "Vereiste game-onderdelen installeren...",
		prerequisitesReady: "Vereisten gereed",
		prerequisitesInstalled: "Game-onderdelen geïnstalleerd",
		prerequisitesFailed: "Waarschuwing bij vereisten",
		prerequisitesFailedMessage: "Vereisten zijn mogelijk niet correct geïnstalleerd",
		browserRequired: "Microsoft Edge vereist",
		signingIn: "Aanmelden...",
		cloudSyncDownFailed: "Cloudopslag downloaden mislukt voor {{store}} ({{error}}). Spelen met lokale opslag.",
		cloudSyncUpFailed: "Cloudopslag uploaden mislukt for {{store}} ({{error}}). Je voortgang is alleen lokaal opgeslagen.",
		errorCircuitBreakerOpen: "{{game_key}} is onlangs {{count}} keer achter elkaar gecrasht. Automatische start uitgesteld.",
		launcherError: "Kan {{game_key}} niet starten ({{error_code}}). Controleer logs voor details.",
		downloadingProton: "Proton downloaden — dit kan enkele minuten duren...",
		downloadingRuntime: "Steam Runtime downloaden — de eerste keer opstarten kan enkele minuten duren...",
		installingUbisoft: "Installeren via Ubisoft Connect",
		installingUbisoftMessage: "Ubisoft Connect openen — start de installatie vanuit het launcher-venster.",
		protonReadyBody: "{{version}} is klaar voor gebruik",
		protonReadyTitle: "Proton gereed",
		protonSwitchedTo: "Nu in gebruik: {{version}}",
		signingInUbisoft: "Aanmelden bij Ubisoft Connect",
		signingInUbisoftMessage: "Ubisoft Connect openen — meld je daar aan en kom dan terug.",
		ubisoftPrefixNotReady: "Ubisoft Connect is niet gereed",
		ubisoftPrefixNotReadyMessage: "Kan de Ubisoft Connect-prefix voor dit spel niet voorbereiden. Probeer opnieuw te installeren; als het blijft mislukken, voer dan opnieuw een bibliotheeksynchronisatie uit.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect is niet gereed",
		ubisoftAuthPrefixNotReadyMessage: "Kan Ubisoft Connect niet voorbereiden voor het aanmelden. Probeer het over een moment opnieuw.",
		ubisoftLaunchIdMissing: "Ubisoft Connect openen",
		ubisoftLaunchIdMissingMessage: "Kon de start-ID van deze game niet herleiden — Ubisoft Connect openen zodat je de game vanuit je bibliotheek kunt starten. Voer opnieuw een bibliotheeksynchronisatie uit om rechtstreeks starten te herstellen."
	},
	updateComplete: "Update voltooid!",
	updateCompleteMessage: "{{title}} is bijgewerkt.",
	updateCancelled: "Update geannuleerd",
	updateCancelledMessage: "Update voor {{title}} is geannuleerd.",
	updateFailed: "Update mislukt",
	updateFailedMessage: "Kan update niet starten.",
	storeError: "Kan niet communiceren met {{store}} ({{errType}})",
	captureLogsStarted: "Logboeken verzamelen...",
	captureLogsStartedMessage: "Diagnosepakket wordt gemaakt in je map Downloads. Dit duurt enkele seconden.",
	captureLogsDone: "Logboeken vastgelegd",
	captureLogsDoneMessage: "Opgeslagen in {{path}} ({{size}}, {{files}} bestanden).",
	captureLogsPartial: "Logboeken vastgelegd (gedeeltelijk)",
	captureLogsPartialMessage: "Opgeslagen in {{path}} ({{size}}, {{files}} bestanden). {{failures}} bestand(en) konden niet worden gelezen.",
	captureLogsFailed: "Kan logboeken niet vastleggen",
	captureLogsBusy: "Er wordt al een logboekopname uitgevoerd",
	captureLogsCopied: "Pad gekopieerd",
	captureLogsCopyFailed: "Kan het pad niet kopiëren",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Ruimte vrijmaken",
		signInToStore: "Aanmelden",
		retrySync: "Opnieuw proberen",
		retryLibrarySync: "Bibliotheek vernieuwen",
		openSaveFolder: "Spelmap weergeven",
		showLogs: "Logs weergeven"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Kan bibliotheek van {{store}} niet vernieuwen ({{error}}). Je bestaande bibliotheek blijft zichtbaar."
	},
	cloudPullStarted: "Cloudopslag downloaden…",
	cloudPushStarted: "Lokale opslag uploaden…",
	cloudSavePathFailed: "Kon opslaglocatie niet instellen",
	cloudSavePathSet: "Opslaglocatie bijgewerkt",
	cloudPullDone: "Cloudopslag gedownload",
	cloudPullFailed: "Downloaden van cloudopslag mislukt",
	cloudPushDone: "Lokale opslag geüpload",
	cloudPushFailed: "Uploaden van lokale opslag mislukt"
};
var uninstallModal$9 = {
	title: "Deïnstallatie bevestigen",
	description: "Weet je zeker dat je {{title}} wilt deïnstalleren?",
	deleteProtonLabel: "Ook Proton-bestanden verwijderen",
	deleteProtonDescription: "Verwijdert compatibiliteitsgegevens en gebruikersbestanden",
	warningTitle: "Waarschuwing:",
	warningBody: "Dit zal permanent spelbestanden verwijderen die zijn opgeslagen in de Wine-prefix. Zorg dat je een back-up hebt.",
	cancel: "Annuleren",
	uninstall: "Deïnstalleren"
};
var languageSettings$9 = {
	title: "TAAL",
	label: "Weergavetaal",
	description: "Taal voor UI en downloads (indien ondersteund)",
	autoDetect: "Auto (Systeem)"
};
var errors$9 = {
	legendaryNotFound: "Epic Games CLI (Legendary) niet gevonden",
	gogdlNotFound: "GOG CLI (gogdl) niet gevonden",
	nileNotFound: "Amazon Games CLI (Nile) niet gevonden",
	authUrlFailed: "Kan authenticatie-URL deïnstalleren",
	authFailed: "Authenticatie mislukt",
	notAuthenticated: "Niet geauthenticeerd. Meld je eerst aan.",
	syncInProgress: "Er is al een synchronisatie bezig",
	syncCancelled: "Synchronisatie geannuleerd door gebruiker",
	gameNotFound: "Game niet gevonden",
	gameNotInstalled: "Game is niet geïnstalleerd",
	installFailed: "Installatie mislukt - zie logs voor details",
	installNoDirectory: "Installatie voltooid, maar kan de gamemap niet vinden",
	uninstallFailed: "Deïnstallatie mislukt",
	lockConflict: "Lock-conflict - probeer het opnieuw",
	unknownStore: "Onbekende winkel",
	shortcutWriteFailed: "Schrijven van snelkoppelingen mislukt",
	backgroundSyncDisabled: "Achtergrondsycnhronisatie uitgeschakeld",
	steamGridDbUnavailable: "SteamGridDB-bibliotheek niet beschikbaar",
	deleteSyncInProgress: "Kan niet verwijderen terwijl er een synchronisatie bezig is",
	alreadyInQueue: "Al in de wachtrij",
	download: {
		diskSpace: "Onvoldoende schijfruimte beschikbaar",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Netwerkfout — controleer je verbinding",
		authExpired: "Aanmelding verlopen — meld je opnieuw aan",
		toolMissing: "Vereist downloadprogramma niet gevonden",
		processFailed: "Download mislukt — probeer het opnieuw",
		directoryNotFound: "Kan geïnstalleerde bestanden niet vinden",
		lockConflict: "Een andere actie is bezig — probeer opnieuw",
		ubisoftClient: "Fout in Ubisoft Connect — probeer de prefix te repareren",
		ubisoftClientNotRunning: "Ubisoft Connect reageert niet",
		ubisoftBootstrap: "Kan Ubisoft Connect-prefix niet configureren",
		ubisoftUmuMissing: "umu-run niet gevonden — vereist voor Ubisoft-games",
		generic: "Installatie mislukt — zie logs voor details"
	},
	unknown: "Onbekende fout",
	steamAppsUnavailable: "API SteamClient.Apps niet beschikbaar"
};
var sync$9 = {
	start: "Bibliotheken synchroniseren",
	force: "Synchronisatie forceren",
	syncing: "Synchroniseren…",
	cancel: "Synchronisatie annuleren",
	cancelling: "Annuleren…",
	preparing: "Voorbereiden…",
	downloadingArtwork: "Artwork downloaden voor {{game}}...",
	fetchingGameLists: "Gamelijsten ophalen...",
	fetchingStore: "{{store}} ophalen ({{current}}/{{total}})...",
	cancelledByUser: "Synchronisatie geannuleerd door gebruiker",
	checkingInstalledGames: "Geïnstalleerde games controleren...",
	sgdbLookup: "{{count}} games opzoeken op SteamGridDB...",
	extractingSteamMetadata: "Steam-metadata uitpakken...",
	fetchingEnhancedMetadata: "Verbeterde metadata ophalen...",
	lookingUpUnifiDB: "unifiDB-metadata ophalen...",
	fetchingMetacriticData: "Metacritic-metadata ophalen...",
	fetchingCompatData: "Compatibiliteitsdata controleren...",
	checkingExistingArtwork: "Bestaand artwork controleren...",
	cleaningOrphanedArtwork: "Verweesd artwork opruimen...",
	retryingMissingArtwork: "{{synced}} / {{total}} games met ontbrekend artwork opnieuw proberen...",
	savingShortcuts: "Snelkoppelingen opslaan...",
	completed: "Synchronisatie voltooid!",
	extractingMetadata: "Metadata uitpakken voor {{game}}...",
	cooldown: "Wacht {{secs}}s",
	artworkPhase: "Artwork {{done}} / {{total}}"
};
var force_sync$9 = {
	migratingOldInstallations: "Geforceerde sync: Oude installaties migreren...",
	fetchingGameLists: "Geforceerde sync: Gamelijsten ophalen...",
	checkingInstalledGames: "Geforceerde sync: Geïnstalleerde games controleren...",
	rewritingShortcuts: "Geforceerde sync: Snelkoppelingen herschrijven...",
	cancelledShortcutsSaved: "Geforceerde sync geannuleerd - snelkoppelingen opgeslagen",
	writingShortcuts: "Geforceerde sync: Snelkoppelingen schrijven...",
	clearingProton: "Geforceerde sync: Proton-compatibiliteit opruimen...",
	completed: "Geforceerde sync voltooid! {{updated}} games bijgewerkt, {{artwork}} artwork opgehaald."
};
var artwork$9 = {
	checking: "Artwork controleren...",
	queueRefresh: "Alle games in de wachtrij plaatsen voor vernieuwen artwork...",
	downloadProgress: "Artwork gedownload: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$9 = {
	greatOnDeck: "Geweldig op Deck",
	allGames: "Alle games",
	installed: "Geïnstalleerd",
	nonSteam: "Niet-Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$9 = {
	error: "Unifideck-fout",
	errorLoadingView: "Laden van de verenigde bibliotheekweergave mislukt",
	checkConsole: "Controleer de browserconsole voor details",
	errorLoadingGames: "Fout bij laden van games",
	tryReloading: "Probeer de plugin opnieuw te laden of controleer de console voor details",
	storeLabel: "Winkel:",
	allStores: "Alle winkels",
	searchPlaceholder: "Games zoeken...",
	library: "Bibliotheek"
};
var gameInfoPanel$9 = {
	loading: "Game-informatie laden...",
	noMetadata: "Geen metadata beschikbaar voor deze game.",
	noTestResults: "Geen gedetailleerde testresultaten beschikbaar.",
	buttons: {
		details: "Details",
		synopsis: "Synopsis",
		storePage: "Winkelpagina",
		dlc: "DLC",
		communityHub: "Community-hub",
		pointsShop: "Points Shop",
		discussions: "Discussies",
		guides: "Handleidingen",
		support: "Ondersteuning",
		install: "Installeren",
		uninstall: "Deïnstalleren",
		cancel: "Annuleren",
		achievements: "Prestaties"
	},
	labels: {
		store: "Winkel:",
		name: "Naam:",
		size: "Grootte:",
		developer: "Ontwikkelaar:",
		publisher: "Uitgever:",
		released: "Uitgebracht:",
		metacritic: "Metacritic:",
		status: "Status:",
		spaceRequired: "Benodigde ruimte",
		lastPlayed: "Laatst gespeeld",
		playTime: "Speeltijd",
		never: "Nooit"
	},
	compatibility: {
		modalTitle: "Steam Deck-compatibiliteit",
		viewOnProtonDb: "Bekijken op ProtonDB",
		close: "Sluiten",
		verified: "GEVERIFIEERD",
		playable: "SPEELBAAR",
		unsupported: "NIET ONDERSTEUND",
		unknown: "ONBEKEND"
	},
	failedToLoadMetadata: "Laden van metadata mislukt",
	lastSession: "Laatste sessie: {{names}} ontgrendeld"
};
var gameDetailsSettings$9 = {
	title: "GAMEDETAILSWEERGAVE",
	simple: "Simpel",
	detailed: "Gedetailleerd"
};
var gogLanguageModal$9 = {
	title: "Taal selecteren",
	description: "Kies welke taal je wilt downloaden voor {{title}}",
	label: "Taal",
	install: "Installeren",
	cancel: "Annuleren"
};
var accountSwitch$9 = {
	title: "Accountwijziging gedetecteerd",
	description: "Er is aangemeld met een ander Steam-account. Wat wil je doen met je Unifideck-gegevens?",
	migrate: "Unifideck-gegevens migreren",
	migrateDescription: "Games en artwork kopiëren van het vorige account",
	freshStart: "Schone start",
	freshStartDescription: "Winkellogins wissen om verbinding te maken met andere Epic/GOG/Amazon/Ubisoft-accounts",
	skip: "Overslaan",
	toastMigrateTitle: "Migratie voltooid",
	toastMigrateBody: "{{shortcuts}} snelkoppelingen en {{artwork}} artworkbestanden gemigreerd",
	toastClearTitle: "Authenticatietokens gewist",
	toastClearBody: "Winkellogins zijn gewist. Je kunt nu inloggen met andere accounts."
};
var ubisoftAuth$9 = {
	title: "Ubisoft Connect-aanmelding",
	description: "Meld je aan via Ubisoft Connect om je bibliotheek te koppelen.",
	signInViaUpc: "Aanmelden via Ubisoft Connect",
	upcDescription: "Ubisoft Connect wordt geopend zodat je kunt inloggen. Zodra je bent aangemeld, wordt je sessie automatisch vastgelegd.",
	launchingUpc: "Ubisoft Connect starten...",
	waitingForLogin: "Wachten op aanmelding...",
	retryLaunch: "Opnieuw proberen",
	cancel: "Annuleren",
	launchFailed: "Kan Ubisoft Connect niet starten. Probeer het opnieuw.",
	signingIn: "Aanmelden...",
	emailLabel: "E-mailadres",
	passwordLabel: "Wachtwoord",
	signIn: "Aanmelden",
	twoFactorTitle: "Tweestapsverificatie",
	twoFactorDescription: "Voer de verificatiecode van je authenticator-app in.",
	twoFactorLabel: "Verificatiecode",
	verify: "Verifiëren",
	verifying: "Verifiëren...",
	errorEmptyFields: "Voer je e-mailadres en wachtwoord in",
	errorEmpty2FA: "Voer de verificatiecode in",
	errorLoginFailed: "Aanmelden mislukt. Controleer je inloggegevens.",
	errorInvalid2FA: "Ongeldige verificatiecode. Probeer het opnieuw.",
	showPassword: "Wachtwoord weergeven",
	hidePassword: "Wachtwoord verbergen",
	doubleLoginWarning: "Je moet twee keer inloggen voor Ubisoft."
};
var relativeDate$9 = {
	today: "Vandaag",
	yesterday: "Gisteren"
};
var common$9 = {
	thisGame: "deze game",
	game: "Game",
	unknown: "Onbekend",
	ok: "OK",
	cancel: "Annuleren",
	working: "Verwerken…",
	loading: "Laden…",
	free: "gratis",
	close: "Sluiten"
};
var info$9 = {
	expandDetails: "Details weergeven",
	collapseDetails: "Details verbergen"
};
var microsoft$9 = {
	notCompatible: "Dit gameformaat is niet compatibel met SteamOS",
	playOnCloud: "In de cloud spelen",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Meld je aan met je Microsoft- / Xbox-account",
	accountConnected: "Microsoft-account verbonden",
	loggedOut: "Afgemeld bij Microsoft Store",
	xcloudNotInstalled: "xCloud-games zijn não lokaal geïnstalleerd",
	chromiumRequired: "Microsoft Edge vereist",
	chromiumRequiredMessage: "Aanmelden vereist Microsoft Edge. Wil je dit automatisch installeren? (Dit hoeft maar één keer te gebeuren).",
	chromiumInstallButton: "Microsoft Edge installeren",
	chromiumInstalling: "Microsoft Edge installeren...",
	chromiumInstalled: "Microsoft Edge geïnstalleerd!",
	chromiumAlreadyInstalled: "Er is al een compatibele browser geïnstalleerd",
	chromiumInstallFailed: "Installatie van Microsoft Edge mislukt. Installeer handmatig: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Time-out bij installatie van Microsoft Edge. Controleer je internetverbinding.",
	browserAlreadyInstalled: "Er is al een compatibele browser geïnstalleerd",
	browserInstalled: "Microsoft Edge succesvol geïnstalleerd!",
	browserInstallFailed: "Installatie van Microsoft Edge mislukt. Installeer handmatig: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak is niet beschikbaar op dit systeem",
	signInRequired: "Meld je aan om te spelen",
	syncSkippedNoSubscription: "Geen actief Xbox Game Pass-abonnement gedetecteerd. Je xCloud-bibliotheek is niet gesynchroniseerd.",
	syncSkippedTierUnknown: "Xbox Game Pass-abonnement gedetecteerd, maar het niveau kon niet worden bepaald. Synchronisatie overgeslagen om te voorkomen dat niet-beschikbare games worden getoond.",
	subscriptionCheckFailed: "Kan Xbox Game Pass-abonnement niet verifiëren. xCloud-synchronisatie overgeslagen. Probeer het opnieuw zodra je verbinding stabiel is.",
	subscriptionDetected: "Actief Xbox Game Pass {{tier}}-abonnement."
};
var playButton$9 = {
	play: "Spelen",
	install: "Installeren",
	cancel: "Annuleren",
	retry: "Opnieuw proberen",
	downloadFailed: "Download mislukt",
	installing: "Installeren",
	controllerConfig: "Controllerconfiguratie",
	appSettings: "App-instellingen"
};
var play$9 = {
	play: "Spelen",
	resume: "Hervatten",
	stop: "Stoppen",
	cancel: "Annuleren",
	cancelling: "Annuleren…",
	update: "Bijwerken",
	uninstall: "Deïnstalleren",
	queued: "In wachtrij",
	downloading: "Downloaden",
	extracting: "Uitpakken",
	verifying: "Verifiëren",
	finalizing: "Installatie afronden",
	cancelConfirmTitle: "Download annuleren?",
	cancelConfirmBody: "Weet je zeker dat je de download van {{title}} wilt annuleren? Gedeeltelijke bestanden worden weggegooid.",
	cancelConfirmConfirm: "Download annuleren",
	cancelConfirmCancel: "Doorgaan met downloaden",
	playOnCloud: "In de cloud spelen",
	cloudSave: {
		autoPullOn: "Cloudopslag wordt automatisch gedownload bij het opstarten.",
		close: "Sluiten",
		download: "Cloudopslag downloaden",
		filesCount: "{{count}} bestanden",
		label: "Cloudopslag",
		menuTitle: "Cloudopslag — {{game}}",
		noCloudData: "Onbekend",
		noCloudSupport: "Geen ondersteuning voor cloudopslag",
		setLocation: "Opslaglocatie instellen",
		setLocationHint: "Kies de map waarin deze game zijn spelbestanden opslaat.",
		setLocationTitle: "Kies de opslagmap van deze game",
		statusCloudAvailable: "Cloudopslag beschikbaar",
		statusNoSaves: "Geen lokale spelbestanden",
		statusSyncing: "Cloudopslag synchroniseren…",
		statusUnresolved: "Opslaglocatie niet gevonden — synchronisatie werkt mogelijk niet voor deze game.",
		unresolvedHint: "Start de game eenmaal op zodat we de opslagmap kunnen detecteren, of stel de locatie handmatig in.",
		upload: "Lokale opslag uploaden"
	},
	exe: {
		browse: "Bladeren…",
		changeFailed: "Kon het uitvoerbare bestand niet wijzigen",
		changed: "Uitvoerbaar bestand gewijzigd",
		defaultTag: "(standaard)",
		menuItem: "Uitvoerbaar bestand wijzigen…",
		reset: "Herstellen naar standaard uitvoerbaar bestand",
		resetButton: "Herstellen naar standaard",
		subtitle: "Kies welk bestand deze game start (bijv. sla een launcher over en start de game direct). De werkmap en cloudopslag worden niet beïnvloed.",
		title: "Uitvoerbaar bestand wijzigen — {{game}}",
		unavailable: "Kon de map van deze game niet lezen."
	}
};
var playMeta$9 = {
	spaceRequired: "Benodigde ruimte",
	installedSize: "Geïnstalleerde grootte",
	lastPlayed: "Laatst gespeeld",
	neverPlayed: "Nooit gespeeld",
	hoursShort: "u",
	minutesShort: "m",
	played: "Gespeeld"
};
var downloads$9 = {
	title: "Downloads",
	empty: "Geen downloads. Installeer games uit je bibliotheek om ze hier te zien.",
	current: "Nu aan het downloaden",
	queued: "In wachtrij",
	finished: "Onlangs voltooid",
	cancel: "Annuleren",
	removeFromQueue: "Verwijderen",
	play: "Spelen",
	outcome: {
		queued: "In wachtrij",
		downloading: "Downloaden",
		completed: "Voltooid",
		cancelled: "Geannuleerd",
		error: "Mislukt"
	}
};
var navigation$9 = {
	loading: "Laden...",
	storesTitle: "Winkels",
	syncTitle: "Bibliotheek sync",
	languageTitle: "Taal"
};
var security$9 = {
	panelTitle: "Beveiliging",
	counters: {
		label: "Tellers",
		empty: "Geen gebeurtenissen waargenomen sinds start"
	},
	log: {
		label: "Auditlogboek",
		entryCount: "{{count}} item",
		entryCount_other: "{{count}} items",
		emptyFiltered: "Geen gebeurtenissen komen overeen met het filter"
	},
	filter: {
		label: "Filter",
		all: "Alle gebeurtenissen"
	},
	timeline: {
		label: "24-uurs tijdlijn ({{count}} gebeurtenis)",
		label_other: "24-uurs tijdlijn ({{count}} gebeurtenissen)",
		axisStart: "-24u",
		axisEnd: "nu"
	},
	relativeTime: {
		seconds: "{{count}}s geleden",
		minutes: "{{count}} min geleden",
		hours: "{{count}}u geleden",
		days: "{{count}}d geleden"
	},
	bruteforce: {
		warningTitle: "Waarschuwing: brute-force detectie (waarschuwingsniveau)",
		escalationTitle: "ALARM: brute-force detectie (escalatieniveau)",
		detail: "{{failures}} mislukte decryptiepogingen in de afgelopen {{window}}s. Drempels: {{warning}} (waarschuwing) / {{escalation}} (escalatie).",
		resetButton: "Bevestigen en detector resetten"
	},
	actions: {
		clearLog: "Logboek en tellers wissen"
	},
	events: {
		tokenEncrypted: "Token gecodeerd",
		tokenDecrypted: "Token gedecodeerd",
		decryptFailed: "Decryptie mislukt",
		tokenFileMigrated: "Tokenbestand gemigreerd",
		legacyPlaintextDetected: "Verouderde platte tekst gedetecteerd",
		authFlowStarted: "Authenticatie gestart",
		authFlowCompleted: "Authenticatie voltooid",
		authFlowFailed: "Authenticatie mislukt",
		permissionsCheck: "Machtigingen gecontroleerd",
		permissionsRepaired: "Machtigingen gerepareerd",
		bruteforceSuspected: "Brute-force vermoed",
		deviceResetDetected: "Apparaatreset gedetecteerd",
		fingerprintInitialized: "Vingerafdruk geïnitialiseerd",
		externalAuthCheckFailed: "Externe authenticatiecontrole mislukt",
		configValidationCompleted: "Configuratievalidatie voltooid",
		configValidationFailed: "Configuratievalidatie mislukt"
	},
	configValidation: {
		degradedTitle: "Ongeldige configuratie: gedegradeerde modus actief",
		detail: "{{count}} validatiefout gedetecteerd bij het starten",
		detail_other: "{{count}} validatiefouten gedetecteerd bij het starten",
		sourceDefaults: "standaardwaarden",
		sourceUser: "gebruikersinstellingen",
		sourceMerged: "samengevoegd",
		overflow: "+ {{count}} andere fout",
		overflow_other: "+ {{count}} andere fouten"
	}
};
var cloudSyncBehavior$9 = {
	_comment: "Instellingenpaneel waarmee de gebruiker per winkel kan kiezen hoe cloudopslag-synchronisatiefouten moeten worden weergegeven. De vijf vermeldingen van store_label komen overeen met de rijen die worden getoond door CloudSyncBehaviorPanel.tsx. Microsoft is bewust weggelaten: xCloud-streaming heeft geen lokale opslag om te synchroniseren.",
	title: "Cloudopslag-synchronisatiegedrag",
	description: "Kies hoe Unifideck moet reageren als het downloaden of uploaden van cloudopslag mislukt bij het starten van een game.",
	loadError: "Kan huidige instellingen niet laden",
	saveError: "Kan wijziging niet opslaan. Probeer het opnieuw",
	store_label: {
		"default": "Standaard (alle winkels)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Stil (alleen logboek)",
		toast: "Melden (pop-upbericht)"
	}
};
var cloudSync$9 = {
	_comment: "Pop-upberichten verzonden door de launcher als cloud-synchronisatie mislukt. De tijdelijke aanduiding voor de fout wordt opgelost via cloudSync.error.<code> via het error_i18n_key-veld van de backend-payload.",
	error: {
		network_unreachable: "Netwerk onbereikbaar",
		auth_expired: "Authenticatie verlopen",
		forbidden: "Toegang geweigerd",
		quota_exceeded: "Limiet voor cloudopslag overschreden",
		server_error: "Winkelserver niet beschikbaar",
		timed_out: "Aanvraag time-out",
		disk_full: "Onvoldoende schijfruimte",
		disk_space_low: "Overslaan: weinig schijfruimte (maak ruimte vrij en probeer opnieuw)",
		permission_denied: "Toegang geweigerd",
		cancelled: "Geannuleerd",
		unknown: "Onverwachte fout"
	},
	unavailableNativeDep: "Cloudopslag is uitgeschakeld op dit systeem — de Python-installatie ({{python}}) mist een vereist onderdeel. Je lokale opslag is niet beïnvloed; herbouw/herinstalleer de plugin na een systeemupdate om de synchronisatie te herstellen."
};
var launchLogs$9 = {
	_comment: "Berichten voor LaunchLogsModal. De modal wordt geopend via de notificatie-actie unifideck://show-logs/<launch_id>, die is gekoppeld aan launcher-foutmeldingen (circuit breaker + LauncherError). Leest de gearchiveerde logs van elke start in ~/.local/share/unifideck/launches/<launch_id>.log en toont ze gekleurd op basis van ernst.",
	title: "Startlogs — {{launchId}}",
	close: "Sluiten",
	copyAll: "Alles kopiëren",
	"export": "Exporteren naar bestand",
	copied: "Gekopieerd naar klembord",
	copyFailed: "Kopiëren mislukt",
	exporting: "Exporteren…",
	exportedTo: "Geëxporteerd naar {{path}}",
	exportFailed: "Exporteren mislukt: {{error}}",
	loading: "Logs laden…",
	notAvailable: "Logs voor deze start zijn niet meer beschikbaar (buiten de bewaarperiode van 7 dagen).",
	error: "Kan logs niet lezen: {{error}}",
	fileInfo: "{{path}} — weergave van {{shown}} van {{total}} regels",
	filterLabel: "Weergeven",
	filterInfo: "Info",
	filterWarning: "Waarschuwing",
	filterError: "Fout",
	nextError: "Volgende fout ({{current}}/{{total}})",
	noErrors: "Geen fouten",
	allFiltered: "Alle regels zijn verborgen door het huidige filter. Schakel ten minste één ernstniveau in om de logboekregels te zien."
};
var settings$9 = {
	_comment: "Actieknoppen van UnifideckSettingsPanel die niet onder specifiekere componenten vallen (LanguageSelector, CloudSyncBehaviorPanel). refreshAll is een knop die sync_all() start voor alle geregistreerde winkels; de bibliotheekweergave toont de synchronisatievoortgang via haar eigen abonnement op SYNC_PROGRESS-gebeurtenissen.",
	storeConnections: "Winkelverbindingen",
	librarySync: "Bibliotheek sync",
	language: "Taal",
	languageAuto: "Automatisch detecteren",
	refreshAll: {
		sectionTitle: "Bibliotheekacties",
		button: "Alle bibliotheken vernieuwen",
		buttonBusy: "Starten…",
		startedTitle: "Bibliotheekvernieuwing gestart",
		startedBody: "Alle winkels worden op de achtergrond gesynchroniseerd.",
		startedSingle: "De bibliotheek van {{store}} wordt op de achtergrond gesynchroniseerd.",
		failedTitle: "Kan bibliotheekvernieuwing niet starten"
	}
};
var saveFolder$9 = {
	_comment: "Berichten voor SaveFolderModal. De modal wordt geopend via de notificatie-actie unifideck://open-save-folder/<store>/<game_id>, meestal getoond bij cloud-synchronisatiefouten wanneer de gebruiker wil controleren wat er lokaal is opgeslagen. count en size worden geïnterpoleerd; truncated wordt alleen getoond als het resultaat is gelimiteerd.",
	title: "Spelmap — {{store}} / {{gameId}}",
	close: "Sluiten",
	copyPath: "Pad kopiëren",
	copyFileList: "Bestandslijst kopiëren",
	depthLabel: "Diepte",
	filterLabel: "Filter",
	loading: "Mapinhoud laden…",
	noSavesYet: "Nog geen lokale spelbestanden gevonden voor deze game. De map wordt aangemaakt bij de volgende succesvolle cloud-synchronisatie.",
	aggregate: "{{count}} bestanden, {{size}} in totaal",
	truncated: "{{count}} andere bestanden ({{size}}) niet weergegeven. Verklein je filter om ze te zien.",
	error: "Kan spelmap niet lezen: {{error}}"
};
var library$9 = {
	_comment: "UI-diensten per game in de bibliotheekweergave, waaronder de circuit breaker-indicator en geforceerde start- / reset-knoppen wanneer starts herhaaldelijk mislukken.",
	circuitBreaker: {
		_comment: "Indeling voor de startfout-circuit breaker. De indicator verschijnt alleen wanneer het aantal fouten in de geconfigureerde periode de circuit_breaker.failures_threshold bereikt (standaard 3 in 600s). Geforceerde start activeert een eenmalige bypass; Reset wist de hele foutgeschiedenis.",
		badgeRecent: "{{count}} recente startfout",
		badgeRecent_other: "{{count}} recente startfouten",
		badgeTooltip: "{{count}} mislukte starts in de afgelopen periode. De circuit breaker heeft automatische pogingen onderbroken.",
		forceLaunchButton: "Geforceerd starten",
		resetButton: "Fouten resetten",
		failureKind: {
			fastBoot: "Snelle start-crash",
			launcherError: "Voorbereidingsfout bij starten"
		}
	}
};
var pickStorage$9 = {
	title: "Installatielocatie",
	customOption: "Kies andere locatie...",
	customLabel: "Aangepaste locatie",
	change: "Wijzigen...",
	gameSize: "Installatiegrootte"
};
var restart$9 = {
	title: "Synchronisatie voltooid!",
	body: "Start Steam opnieuw op om je nieuwe games en artwork toe te passen",
	confirm: "Nu herstarten",
	later: "Later"
};
var updater$9 = {
	checkButton: "Controleren op updates",
	checkCompleteMessage: "Informatie over nieuwste versie succesvol opgehaald.",
	checkCompleteTitle: "Updatecontrole voltooid",
	checkFailedTitle: "Controle mislukt",
	checkingButton: "Controleren...",
	currentTitle: "Huidige",
	downgradeButton: "Downgraden naar v{{version}}",
	installFailedTitle: "Installatie mislukt",
	installTimeoutMessage: "Geen reactie van Decky Loader — de installatie is mogelijk vastgelopen of mislukt. Probeer het opnieuw.",
	installedLabel: "geïnstalleerd",
	installingButton: "Installeren...",
	installingTitle: "Plugin installeren",
	latestLabel: "nieuwste",
	modalTitle: "UNIFIDECK v{{version}} — Release Notes",
	noBackend: "Decky-backend is niet beschikbaar.",
	noReleaseNotes: "Geen release-notes beschikbaar.",
	reinstallButton: "Opnieuw installeren v{{version}}",
	refreshFailedMessage: "Kan de informatie over de nieuwste versie niet verifiëren. Controleer je verbinding en probeer het opnieuw.",
	releaseGoneMessage: "Deze release is niet meer beschikbaar. Controleer op updates en selecteer opnieuw.",
	releaseNotesButton: "Release-notes",
	stageDownload: "Downloaden…",
	stageFinish: "Afronden…",
	stageInstall: "Installeren…",
	stageOpen: "Pakket lezen…",
	stageParse: "Verifiëren…",
	stageRemove: "Oude versie verwijderen…",
	stageStart: "Starten…",
	stageWorking: "Installeren…",
	titleLoading: "Versie controleren",
	typeDowngrade: "Downgraden naar",
	typeReinstall: "Opnieuw installeren van",
	typeUpdate: "Bijwerken naar",
	updateButton: "Bijwerken naar v{{version}}"
};
var achievements$9 = {
	unlockedTitle: "Prestatie ontgrendeld",
	unlockedBody: "{{name}}"
};
var achievementsModal$9 = {
	title: "Prestaties",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Vernieuwen",
	close: "Sluiten",
	loading: "Prestaties laden…",
	empty: "Dit spel heeft geen prestaties.",
	hidden: "Verborgen prestatie",
	unlockedOn: "Ontgrendeld op {{date}}",
	error: "Kan prestaties niet laden.",
	errorOffline: "Maak verbinding met internet om prestaties te bekijken.",
	errorAuth: "Meld je aan bij de store van dit spel om prestaties te bekijken.",
	errorUnavailable: "Prestaties zijn niet beschikbaar voor dit spel."
};
var collectionSettings$9 = {
	title: "STEAM-COLLECTIES",
	enabled: "Collecties ingeschakeld",
	disabled: "Collecties uitgeschakeld"
};
var nlNL = {
	_comment: _comment$9,
	tabs: tabs$9,
	auth: auth$9,
	storage: storage$9,
	storageSettings: storageSettings$9,
	gameGrid: gameGrid$9,
	downloadsTab: downloadsTab$9,
	cloudSave: cloudSave$9,
	storeConnections: storeConnections$9,
	librarySync: librarySync$9,
	cleanup: cleanup$9,
	captureLogs: captureLogs$9,
	installButton: installButton$9,
	confirmModals: confirmModals$9,
	toasts: toasts$9,
	uninstallModal: uninstallModal$9,
	languageSettings: languageSettings$9,
	errors: errors$9,
	sync: sync$9,
	force_sync: force_sync$9,
	artwork: artwork$9,
	deckTabs: deckTabs$9,
	unifiedLibrary: unifiedLibrary$9,
	gameInfoPanel: gameInfoPanel$9,
	gameDetailsSettings: gameDetailsSettings$9,
	gogLanguageModal: gogLanguageModal$9,
	accountSwitch: accountSwitch$9,
	ubisoftAuth: ubisoftAuth$9,
	relativeDate: relativeDate$9,
	common: common$9,
	info: info$9,
	microsoft: microsoft$9,
	playButton: playButton$9,
	play: play$9,
	playMeta: playMeta$9,
	downloads: downloads$9,
	navigation: navigation$9,
	security: security$9,
	cloudSyncBehavior: cloudSyncBehavior$9,
	cloudSync: cloudSync$9,
	launchLogs: launchLogs$9,
	settings: settings$9,
	saveFolder: saveFolder$9,
	library: library$9,
	pickStorage: pickStorage$9,
	restart: restart$9,
	updater: updater$9,
	achievements: achievements$9,
	achievementsModal: achievementsModal$9,
	collectionSettings: collectionSettings$9
};

var _comment$8 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$8 = {
	settings: "Ustawienia",
	downloads: "Pobieranie"
};
var auth$8 = {
	connect: "Połącz",
	disconnect: "Odłącz",
	status: {
		connected: "Połączono",
		disconnected: "Nie połączono",
		checking: "Sprawdzanie…",
		error: "Błąd — sprawdź logi"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$8 = {
	title: "Pamięć",
	defaultUpdated: "Zaktualizowano domyślną pamięć",
	isDefault: "Domyślna",
	setDefault: "Ustaw jako domyślną",
	customPath: "Ścieżka niestandardowa",
	up: "↑ W górę",
	refresh: "Odśwież",
	useThisPath: "Użyj tej ścieżki"
};
var storageSettings$8 = {
	title: "Ustawienia pobierania",
	installLocationLabel: "Lokalizacja instalacji",
	installLocationDescription: "Gdzie będą pobierane nowe gry",
	pathLabel: "Ścieżka",
	loading: "Ładowanie opcji pamięci...",
	toastUpdatedTitle: "Zaktualizowano lokalizację pamięci",
	toastUpdatedBody: "Nowe gry będą instalowane w {{location}}",
	toastFailedTitle: "Błąd aktualizacji",
	toastFailedBody: "{{error}}",
	unknown: "Nieznana",
	browseButton: "Przeglądaj...",
	clearCustom: "Wyczyść lokalizację niestandardową",
	customPathSet: "Ustawiono niestandardową lokalizację instalacji",
	newFolder: "Nowy folder",
	newFolderCreate: "Utwórz",
	selectFolder: "Użyj tego folderu",
	emptyDirectory: "Pusty folder",
	showHidden: "Pokaż ukryte",
	selectDevice: "Wybierz urządzenie",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Modyfikacja (najnowsze)",
	sortModifiedOldest: "Modyfikacja (najstarsze)",
	sortCreatedNewest: "Utworzenie (najnowsze)",
	sortCreatedOldest: "Utworzenie (najstarsze)",
	sortSizeLargest: "Rozmiar (największe)",
	sortSizeSmallest: "Rozmiar (najmniejsze)",
	"validationErrors.notAbsolute": "Ścieżka musi być absolutna",
	"validationErrors.systemDir": "Nie można użyć katalogu systemowego",
	"validationErrors.notWritable": "Lokalizacja nie jest zapisywalna",
	"validationErrors.notFound": "Katalog nie istnieje",
	"validationErrors.homeRoot": "Nie można użyć katalogu domowego root",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB wolnego)"
};
var gameGrid$8 = {
	loading: "Ładowanie gier...",
	noGamesFound: "Nie znaleziono gier",
	noGamesHint: "Spróbuj zsynchronizować biblioteki w ustawieniach Unifideck",
	showingGames: "Wyświetlanie {{count}} gier",
	installed: "Zainstalowane",
	notInstalled: "Niezainstalowane",
	playtime: "Czas gry: {{hours}} godz. {{minutes}} min."
};
var downloadsTab$8 = {
	currentDownload: "AKTUALNIE POBIERANE",
	queuedDownloads: "W KOLEJCE ({{count}})",
	recentlyCompleted: "OSTATNIO UKOŃCZONE",
	noActiveDownloads: "Brak aktywnych pobrań",
	noDownloads: "Brak pobrań. Zainstaluj gry ze swojej biblioteki, aby zobaczyć je tutaj.",
	loadingLabel: "Ładowanie...",
	loadingMessage: "Pobieranie kolejki...",
	confirmCancelTitle: "Potwierdź anulowanie",
	confirmCancelDescription: "Czy na pewno chcesz anulować pobieranie gry {{game}}?",
	confirmCancelYes: "Tak",
	confirmCancelNo: "Nie",
	cancelDownload: "Anuluj",
	toastDownloadCancelledTitle: "Pobieranie anulowane",
	toastDownloadCancelledBody: "Pobieranie zostało usunięte z kolejki.",
	toastCancelFailedTitle: "Anulowanie nie powiodło się",
	toastCancelFailedBody: "Nie można anulować pobierania: {{error}}",
	status: {
		queued: "W kolejce",
		downloading: "Pobieranie",
		completed: "Ukończono",
		cancelled: "Anulowano",
		error: "Błąd"
	},
	preparingDownload: "Przygotowywanie pobierania...",
	phaseExtracting: "Rozpakowywanie {{game}}...",
	phaseVerifying: "Weryfikowanie {{game}}...",
	internalStorage: "Pamięć wewnętrzna ({{freeSpace}} GB wolnego)",
	sdCard: "Karta SD ({{freeSpace}} GB wolnego)",
	customLocation: "Niestandardowa: {{path}} ({{freeSpace}} GB wolnego)",
	extractingLabel: "Rozpakowywanie...",
	verifyingLabel: "Weryfikowanie...",
	updateQueuedLabel: "Aktualizacja w kolejce",
	downloadQueuedLabel: "Pobieranie w kolejce",
	downloadingUpdateLabel: "Pobieranie aktualizacji",
	downloadingLabel: "Pobieranie...",
	finalizingInstallation: "Finalizowanie instalacji...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "Pozostały czas: {{eta}}",
	preparingMessage: "Inicjowanie prefiksu i synchronizowanie zapisów w chmurze...",
	preparingLabel: "Przygotowywanie gry...",
	installingViaUpcLabel: "Instalowanie przez Ubisoft Connect",
	extractingMessage: "Wyodrębnianie plików...",
	verifyingMessage: "Weryfikowanie plików... {{pct}}%",
	installingViaUpcMessage: "Postępuj zgodnie z oknem Ubisoft Connect, aby zakończyć instalację..."
};
var cloudSave$8 = {
	title: "Konflikt zapisów w chmurze",
	description: "Zapisy lokalne różnią się od zapisów w chmurze dla {{game}}",
	local: "Lokalny:",
	cloud: "Chmura:",
	newer: "(nowszy)",
	useCloud: "Użyj chmury",
	useLocal: "Użyj lokalnego",
	unknownDate: "Nieznana",
	toastUsingCloudTitle: "Używanie zapisów z chmury",
	toastUsingCloudBody: "Pobieranie zapisów z chmury dla {{game}}",
	toastUsingLocalTitle: "Używanie zapisów lokalnych",
	toastUsingLocalBody: "Przesyłanie zapisów lokalnych dla {{game}}",
	toastErrorTitle: "Błąd",
	toastErrorBody: "Nie udało się rozwiązać konfliktu",
	filesSize: "{{count}} plików · {{size}}",
	uploadBlockedEmpty: "Pominięto przesyłanie do chmury dla {{game}} — nie znaleziono lokalnych zapisów",
	uploadSkippedTitle: "Pominięto przesyłanie do chmury",
	uploadSkippedBody: "Brak lokalnych zapisów do przesłania dla {{game}}."
};
var storeConnections$8 = {
	title: "POŁĄCZENIA SKLEPÓW",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Połączono",
	checking: "Sprawdzanie...",
	authenticate: "Zaloguj się",
	legendaryNotInstalled: "⚠️ Instalowanie Epic Games CLI...",
	nileNotInstalled: "⚠️ Nie znaleziono Amazon Games CLI",
	error: "Błąd - sprawdź logi"
};
var librarySync$8 = {
	title: "SYNCHRONIZACJA BIBLIOTEKI",
	syncLibraries: "Synchronizuj biblioteki",
	forceSync: "Wymuś synchronizację",
	syncing: "Synchronizacja...",
	cancelSync: "Anuluj synchronizację",
	retryStatusCheck: "Ponów sprawdzenie statusu",
	gamesSynced: "{{synced}} / {{total}} gier zsynchronizowanych",
	artworkDownloaded: "{{synced}} / {{total}} okładek pobranych",
	metadataExtracted: "{{synced}} / {{total}} rekordów metadanych wyodrębnionych",
	steamMetadataDownloaded: "Metadane Steam: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "Metadane unifiDB: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Dane Metacritic: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "Metadane RAWG: {{synced}} / {{total}}",
	compatChecked: "Dane zgodności: {{synced}} / {{total}}",
	cancelling: "Anulowanie…",
	error: "Błąd"
};
var cleanup$8 = {
	title: "Czyszczenie",
	deleteAll: "Usuń wszystkie biblioteki i pamięć podręczną UNIFIDECK",
	modalTitle: "Usunąć wszystkie dane Unifideck?",
	modalDescription: "Spowoduje to usunięcie wszystkich skrótów Unifideck, okładek Steam, zapisanych logowań sklepowych i pamięci podręcznej. Zainstalowane gry pozostaną na dysku i będą mogły być ponownie zsynchronizowane. Włącz tryb destrukcyjny poniżej, aby usunąć również pliki gier.",
	deleteFilesLabel: "Usuń również zainstalowane pliki gier (Destrukcyjne)",
	deleteFilesDescription: "Usuwa katalog instalacyjny na dysku dla każdej gry Unifideck w bezpiecznym folderze.",
	confirmDelete: "Tak, usuń wszystko",
	cancel: "Anuluj",
	deleting: "Usuwanie..."
};
var captureLogs$8 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (pl-PL).",
	title: "Przechwyć dzienniki",
	description: "Zbiera wszystkie dzienniki Unifideck do jednego pliku zip w folderze Pobrane, aby można je było dołączyć do zgłoszenia błędu.",
	capture: "Przechwyć dzienniki",
	capturing: "Zbieranie dzienników...",
	savedTo: "Zapisano w folderze Pobrane",
	bundleInfo: "{{files}} plików · {{size}}",
	partialNote: "Nie udało się odczytać {{count}} pliku. Paczka nadal jest przydatna.",
	partialNote_other: "Nie udało się odczytać {{count}} plików. Paczka nadal jest przydatna.",
	copyPath: "Kopiuj ścieżkę",
	modalTitle: "Utworzyć paczkę diagnostyczną?",
	modalDescription: "Unifideck zbierze swoje dzienniki do jednego pliku zip w folderze Pobrane, aby można je było dołączyć do zgłoszenia błędu.",
	modalIncludes: "Zawiera: dzienniki wtyczki i uruchomień gier, historię synchronizacji i instalacji, tytuły twoich gier i ścieżki instalacji oraz informacje o urządzeniu, takie jak model, system operacyjny i nośniki.",
	modalExcludes: "Nigdy nie zawiera: twoich haseł, tokenów logowania do sklepów ani plików cookie przeglądarki.",
	modalConfirm: "Utwórz paczkę",
	cancel: "Anuluj"
};
var installButton$8 = {
	play: "Graj",
	resume: "Wznów",
	install: "Zainstaluj {{title}}",
	installNative: "Zainstaluj",
	uninstall: "Odinstaluj {{title}}",
	cancel: "✖ Anuluj",
	processing: "Przetwarzanie...",
	checking: "Sprawdzanie...",
	size: "{{size}} GB",
	update: "AKTUALIZUJ",
	updateAvailable: "DOSTĘPNA AKTUALIZACJA",
	updateAvailableDescription: "Nowa wersja jest gotowa do instalacji."
};
var confirmModals$8 = {
	installTitle: "Potwierdź instalację",
	installDescription: "Czy na pewno chcesz zainstalować {{title}}?",
	uninstallTitle: "Potwierdź odinstalowanie",
	uninstallDescription: "Czy na pewno chcesz odinstalować {{title}}?",
	cancelTitle: "Potwierdź anulowanie",
	cancelDescription: "Czy na pewno chcesz anulować pobieranie {{title}}?",
	forceSyncTitle: "Ustawienia wymuszonej synchronizacji",
	forceSyncDescription: "Czy chcesz ponownie zsynchronizować wszystkie okładki? Spowoduje to nadpisanie wszelkich ręcznych zmian. Wybierz 'Zachowaj okładki', aby pobrać tylko brakujące.",
	resyncArtwork: "Ponowna synchronizacja okładek",
	keepArtwork: "Zachowaj okładki",
	steamRestartTitle: "Synchronizacja zakończona!",
	steamRestartDescription: "Uruchom ponownie Steam, aby zastosować nowe gry i okładki",
	steamRestartDescriptionStore: "Uruchom ponownie Steam, aby zastosować nowe gry i okładki {{store}}",
	steamRestartCleanupTitle: "Czyszczenie zakończone!",
	steamRestartCleanupDescription: "Uruchom ponownie Steam, aby całkowicie usunąć skasowane gry i okładki",
	restartNow: "Uruchom ponownie teraz",
	later: "Później",
	ubisoftInstallTitle: "Wymagany Ubisoft Connect",
	ubisoftInstallDescription: "Spowoduje to uruchomienie Ubisoft Connect, który jest wymagany do zainstalowania {{title}}.",
	ubisoftInstallConfirm: "Otwórz Ubisoft Connect",
	yes: "Tak",
	no: "Nie",
	amazonUpdateTitle: "Wymagane pełne ponowne pobranie",
	amazonUpdateDescription: "Aktualizacje Amazon wymagają pełnego pobrania gry {{title}} na nowo. Może to chwilę potrwać. Kontynuować?",
	updateConfirm: "Aktualizuj"
};
var toasts$8 = {
	downloadStarted: "Pobieranie rozpoczęte. Sprawdź UNIFIDECK > Pobieranie",
	downloadQueued: "Pobieranie w kolejce",
	downloadQueuedBody: "Gra {{title}} została dodana do kolejki.",
	downloadFailedUnknown: "Nieznany błąd.",
	updateQueued: "Aktualizacja w kolejce",
	uninstallDone: "Odinstalowywanie zakończone.",
	ubisoftLauncherOpening: "Otwieranie Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect otwiera się dla {{title}}. Zainstaluj grę tam, a następnie wróć, by grać.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect jest już otwarty dla tej instalacji.",
	ubisoftLauncherOpenFailed: "Nie można otworzyć Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Nie udało się otworzyć Ubisoft Connect w celu instalacji.",
	multipartDetected: "Wykryto pobieranie wieloczęściowe",
	multipartMessage: "Prosimy o cierpliwość i oczekiwanie na ukończenie",
	downloadFailed: "Pobieranie nie powiodło się",
	downloadFailedMessage: "Nie można rozpocząć pobierania.",
	downloadCancelled: "Pobieranie anulowane",
	downloadCancelledMessage: "Anulowano pobieranie gry {{title}}.",
	cancelFailed: "Anulowanie nie powiodło się",
	cancelFailedMessage: "Nie można anulować pobierania.",
	uninstalling: "Odinstalowywanie...",
	uninstallingMessage: "Odinstalowywanie {{title}}...",
	uninstallingMessageProton: "Odinstalowywanie {{title}} oraz plików Proton...",
	uninstallComplete: "Odinstalowywanie zakończone",
	uninstallCompleteMessage: "Gra {{title}} została usunięta.",
	uninstallCompleteMessageProton: "Gra {{title}} została usunięta (wraz z plikami Proton).",
	installComplete: "Instalacja zakończona!",
	installCompleteMessage: "Gra {{title}} jest gotowa do uruchomienia.",
	syncComplete: "Synchronizacja zakończona!",
	syncCompleteMessage: "Dodano {{count}} gier. URUCHOM PONOWNIE STEAM (wyjdź całkowicie, nie tylko powróć do trybu gry), aby zobaczyć je w bibliotece.",
	forceSyncComplete: "Wymuszona synchronizacja zakończona!",
	forceSyncCompleteMessage: "Zaktualizowano {{count}} gier. URUCHOM PONOWNIE STEAM, aby zobaczyć zmiany.",
	syncCancelled: "Synchronizacja anulowana",
	authTimeout: "Limit czasu uwierzytelniania przekroczony",
	authTimeoutMessage: "Uwierzytelnianie {{store}} przekroczyło limit czasu. Spróbuj ponownie.",
	authFailed: "Uwierzytelnianie nie powiodło się",
	authFailedMessage: "Nie udało się rozpocząć uwierzytelniania",
	authError: "Błąd uwierzytelniania",
	authConnected: "Połączono ze sklepem {{store}}!",
	authConnectedMessage: "Pomyślnie uwierzytelniono w {{store}}",
	cleanupStarted: "Czyszczenie rozpoczęte",
	cleanupStartedMessage: "Usuwanie skrótów, okładek i zapisanych logowań. To może chwilę potrwać…",
	cleanupSuccessful: "Czyszczenie zakończone sukcesem",
	cleanupSuccessfulMessage: "Usunięto {{games}} skrótów, {{artwork}} plików okładek, wylogowano z {{stores}} sklepów, usunięto {{files}} folderów instalacyjnych. Pamięć podręczna wyczyszczona.",
	cleanupNoop: "Brak elementów do wyczyszczenia",
	cleanupNoopMessage: "Unifideck nie ma skrótów, okładek ani zapisanych danych logowania do usunięcia.",
	deleteFailed: "Usuwanie nie powiodło się",
	repairPrefixSuccess: "Naprawiono prefiks",
	repairPrefixSuccessMessage: "Prefiks Ubisoft Connect został odbudowany",
	repairPrefixFailed: "Naprawa nie powiodła się",
	repairPrefixFailedMessage: "Nie można naprawić prefiksu: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Błąd sieci",
		launchRetry: "Ponawianie uruchamiania",
		retryingUmu: "Ponowna próba UMU za {{seconds}} s (próba {{attempt}}/{{max}})...",
		protonUpgrade: "Aktualizacja Protona",
		resettingPrefix: "Resetowanie prefiksu dla {{version}}...",
		firstTimeSetup: "Pierwsza konfiguracja",
		initializingPrefix: "Inicjowanie prefiksu Wine...",
		setupCompleteTitle: "Konfiguracja zakończona",
		prefixInitialized: "Prefiks Wine zainicjowany",
		setupFallback: "Konfiguracja",
		fallbackInitialization: "Inicjalizacja zapasowa...",
		launchingGame: "Uruchamianie gry",
		startingEpicGame: "Uruchamianie gry Epic...",
		startingAmazonGame: "Uruchamianie gry Amazon...",
		startingGogGame: "Uruchamianie gry GOG...",
		startingUbisoftGame: "Uruchamianie gry Ubisoft...",
		dependenciesTitle: "Instalowanie zależności",
		installingRedistributables: "Instalowanie wymaganych bibliotek...",
		dependenciesReady: "Zależności gotowe",
		redistributablesInstalled: "Biblioteki zainstalowane",
		dependenciesStatus: "Zależności",
		checkLogs: "Konfiguracja zakończona (sprawdź logi po szczegóły)",
		installingProton: "Instalowanie Protona",
		attemptingInstall: "Próba instalacji {{version}}...",
		epicAuthRequired: "Wymagane logowanie Epic",
		epicAuthRequiredMessage: "Zaloguj się do Epic Games w ustawieniach Unifideck",
		installingRedist: "Instalowanie bibliotek",
		installingRedistMessage: "Uruchamianie pierwszej instalacji bibliotek GOG...",
		setupFailed: "Konfiguracja nie powiodła się",
		setupFailedMessage: "Instalacja nie powiodła się, sprawdź logi",
		prerequisitesTitle: "Instalowanie składników",
		installingPrerequisites: "Instalowanie wymaganych składników gry...",
		prerequisitesReady: "Składniki gotowe",
		prerequisitesInstalled: "Składniki gry zainstalowane",
		prerequisitesFailed: "Ostrzeżenie o składnikach",
		prerequisitesFailedMessage: "Składniki gry mogły nie zostać zainstalowane poprawnie",
		browserRequired: "Wymagany Microsoft Edge",
		signingIn: "Logowanie...",
		cloudSyncDownFailed: "Błąd pobierania zapisów w chmurze dla {{store}} ({{error}}). Uruchamianie z zapisem lokalnym.",
		cloudSyncUpFailed: "Błąd przesyłania zapisów w chmurze dla {{store}} ({{error}}). Twój postęp jest zapisany tylko lokalnie.",
		errorCircuitBreakerOpen: "Gra {{game_key}} nie uruchomiła się poprawnie {{count}} razy z rzędu. Automatyczne próby wstrzymane.",
		launcherError: "Nie udało się uruchomić {{game_key}} ({{error_code}}). Sprawdź logi.",
		downloadingProton: "Pobieranie Proton — może to potrwać kilka minut...",
		downloadingRuntime: "Pobieranie Steam Runtime — pierwsze uruchomienie może potrwać kilka minut...",
		installingUbisoft: "Instalowanie przez Ubisoft Connect",
		installingUbisoftMessage: "Otwieranie Ubisoft Connect — rozpocznij instalację w oknie launchera.",
		protonReadyBody: "{{version}} jest gotowy do użycia",
		protonReadyTitle: "Proton gotowy",
		protonSwitchedTo: "Teraz używasz {{version}}",
		signingInUbisoft: "Logowanie do Ubisoft Connect",
		signingInUbisoftMessage: "Otwieranie Ubisoft Connect — zaloguj się tam, a następnie wróć.",
		ubisoftPrefixNotReady: "Ubisoft Connect nie jest gotowy",
		ubisoftPrefixNotReadyMessage: "Nie udało się przygotować prefiksu Ubisoft Connect dla tej gry. Spróbuj zainstalować ponownie; jeśli błąd się powtarza, ponownie uruchom synchronizację biblioteki.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect nie jest gotowy",
		ubisoftAuthPrefixNotReadyMessage: "Nie udało się przygotować Ubisoft Connect do logowania. Spróbuj ponownie za chwilę.",
		ubisoftLaunchIdMissing: "Otwieranie Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "Nie udało się ustalić identyfikatora uruchamiania tej gry — otwieranie Ubisoft Connect, abyś mógł uruchomić ją z biblioteki. Ponownie uruchom synchronizację biblioteki, aby naprawić bezpośrednie uruchamianie."
	},
	updateComplete: "Aktualizacja zakończona!",
	updateCompleteMessage: "Gra {{title}} została zaktualizowana.",
	updateCancelled: "Aktualizacja anulowana",
	updateCancelledMessage: "Aktualizacja dla {{title}} została anulowana.",
	updateFailed: "Aktualizacja nie powiodła się",
	updateFailedMessage: "Nie udało się rozpocząć aktualizacji.",
	storeError: "Błąd komunikacji ze sklepem {{store}} ({{errType}})",
	captureLogsStarted: "Zbieranie dzienników...",
	captureLogsStartedMessage: "Tworzenie paczki diagnostycznej w folderze Pobrane. Zajmie to kilka sekund.",
	captureLogsDone: "Dzienniki przechwycone",
	captureLogsDoneMessage: "Zapisano w {{path}} ({{size}}, plików: {{files}}).",
	captureLogsPartial: "Dzienniki przechwycone (częściowo)",
	captureLogsPartialMessage: "Zapisano w {{path}} ({{size}}, plików: {{files}}). Nie udało się odczytać plików: {{failures}}.",
	captureLogsFailed: "Nie udało się przechwycić dzienników",
	captureLogsBusy: "Przechwytywanie dzienników już trwa",
	captureLogsCopied: "Ścieżka skopiowana",
	captureLogsCopyFailed: "Nie udało się skopiować ścieżki",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Zwolnij miejsce",
		signInToStore: "Zaloguj się",
		retrySync: "Ponów",
		retryLibrarySync: "Odśwież bibliotekę",
		openSaveFolder: "Pokaż folder zapisu",
		showLogs: "Pokaż logi"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Nie można odświeżyć biblioteki {{store}} ({{error}}). Bieżąca biblioteka pozostaje widoczna."
	},
	cloudPullStarted: "Pobieranie zapisu z chmury…",
	cloudPushStarted: "Przesyłanie lokalnego zapisu…",
	cloudSavePathFailed: "Nie można ustawić lokalizacji zapisu",
	cloudSavePathSet: "Zaktualizowano lokalizację zapisu",
	cloudPullDone: "Zapis z chmury pobrany",
	cloudPullFailed: "Pobieranie zapisu z chmury nie powiodło się",
	cloudPushDone: "Lokalny zapis przesłany",
	cloudPushFailed: "Przesyłanie lokalnego zapisu nie powiodło się"
};
var uninstallModal$8 = {
	title: "Potwierdź odinstalowanie",
	description: "Czy na pewno chcesz odinstalować {{title}}?",
	deleteProtonLabel: "Usuń również pliki Protona",
	deleteProtonDescription: "Usuwa dane zgodności i pliki użytkownika",
	warningTitle: "Ostrzeżenie:",
	warningBody: "Spowoduje to trwałe usunięcie zapisów gry z prefiksu Wine. Upewnij się, że masz ich kopię.",
	cancel: "Anuluj",
	uninstall: "Odinstaluj"
};
var languageSettings$8 = {
	title: "JĘZYK",
	label: "Język interfejsu",
	description: "Język interfejsu i pobierania gier (jeśli obsługiwany)",
	autoDetect: "Auto (System)"
};
var errors$8 = {
	legendaryNotFound: "Nie znaleziono Epic Games CLI (Legendary)",
	gogdlNotFound: "Nie znaleziono GOG CLI (gogdl)",
	nileNotFound: "Nie znaleziono Amazon Games CLI (Nile)",
	authUrlFailed: "Nie udało się pobrać URL uwierzytelniania",
	authFailed: "Uwierzytelnianie nie powiodło się",
	notAuthenticated: "Brak uwierzytelnienia. Zaloguj się najpierw.",
	syncInProgress: "Synchronizacja jest już w toku",
	syncCancelled: "Synchronizacja anulowana przez użytkownika",
	gameNotFound: "Nie znaleziono gry",
	gameNotInstalled: "Gra nie jest zainstalowana",
	installFailed: "Instalacja nie powiodła się - sprawdź logi",
	installNoDirectory: "Instalacja zakończona, lecz nie znaleziono katalogu gry",
	uninstallFailed: "Odinstalowanie nie powiodło się",
	lockConflict: "Konflikt blokady - spróbuj ponownie",
	unknownStore: "Nieznany sklep",
	shortcutWriteFailed: "Nie udało się zapisać skrótów",
	backgroundSyncDisabled: "Synchronizacja w tle wyłączona",
	steamGridDbUnavailable: "Biblioteka SteamGridDB niedostępna",
	deleteSyncInProgress: "Nie można usunąć podczas synchronizacji",
	alreadyInQueue: "Już w kolejce",
	download: {
		diskSpace: "Brak wystarczającej ilości miejsca na dysku",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Błąd sieci — sprawdź swoje połączenie",
		authExpired: "Logowanie wygasło — zaloguj się ponownie",
		toolMissing: "Nie znaleziono wymaganego narzędzia pobierania",
		processFailed: "Pobieranie nie powiodło się — spróbuj ponownie",
		directoryNotFound: "Nie można zlokalizować zainstalowanych plików",
		lockConflict: "Inna operacja jest w toku — spróbuj ponownie",
		ubisoftClient: "Błąd Ubisoft Connect — spróbuj naprawić prefiks",
		ubisoftClientNotRunning: "Klient Ubisoft Connect nie odpowiada",
		ubisoftBootstrap: "Nie udało się skonfigurować prefiksu Ubisoft Connect",
		ubisoftUmuMissing: "Nie znaleziono umu-run — wymagane dla gier Ubisoft",
		generic: "Instalacja nie powiodła się — sprawdź logi"
	},
	unknown: "Nieznany błąd",
	steamAppsUnavailable: "API SteamClient.Apps niedostępne"
};
var sync$8 = {
	start: "Synchronizuj biblioteki",
	force: "Wymuś synchronizację",
	syncing: "Synchronizacja…",
	cancel: "Anuluj synchronizację",
	cancelling: "Anulowanie…",
	preparing: "Przygotowywanie…",
	downloadingArtwork: "Pobieranie okładki dla {{game}}...",
	fetchingGameLists: "Pobieranie list gier...",
	fetchingStore: "Pobieranie {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Synchronizacja anulowana przez użytkownika",
	checkingInstalledGames: "Sprawdzanie zainstalowanych gier...",
	sgdbLookup: "Szukanie {{count}} gier w bazie SteamGridDB...",
	extractingSteamMetadata: "Wyodrębnianie metadanych Steam...",
	fetchingEnhancedMetadata: "Pobieranie rozszerzonych metadanych...",
	lookingUpUnifiDB: "Pobieranie metadanych unifiDB...",
	fetchingMetacriticData: "Pobieranie danych Metacritic...",
	fetchingCompatData: "Sprawdzanie danych zgodności...",
	checkingExistingArtwork: "Sprawdzanie istniejących okładek...",
	cleaningOrphanedArtwork: "Usuwanie osieroconych okładek...",
	retryingMissingArtwork: "Ponowna próba dla {{synced}} / {{total}} gier z brakującymi okładkami...",
	savingShortcuts: "Zapisywanie skrótów...",
	completed: "Synchronizacja ukończona!",
	extractingMetadata: "Wyodrębnianie metadanych dla {{game}}...",
	cooldown: "Odczekaj {{secs}} s",
	artworkPhase: "Okładki {{done}} / {{total}}"
};
var force_sync$8 = {
	migratingOldInstallations: "Wymuszona synchronizacja: Migracja starych instalacji...",
	fetchingGameLists: "Wymuszona synchronizacja: Pobieranie list gier...",
	checkingInstalledGames: "Wymuszona synchronizacja: Sprawdzanie zainstalowanych gier...",
	rewritingShortcuts: "Wymuszona synchronizacja: Przepisywanie skrótów...",
	cancelledShortcutsSaved: "Wymuszona synchronizacja anulowana - skróty zapisane",
	writingShortcuts: "Wymuszona synchronizacja: Zapisywanie skrótów...",
	clearingProton: "Wymuszona synchronizacja: Czyszczenie zgodności Protona...",
	completed: "Wymuszona synchronizacja ukończona! Zaktualizowano {{updated}} gier, pobrano {{artwork}} okładek."
};
var artwork$8 = {
	checking: "Sprawdzanie okładek...",
	queueRefresh: "Kolejkowanie wszystkich gier do odświeżenia okładek...",
	downloadProgress: "Pobrane okładki: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$8 = {
	greatOnDeck: "Zgodne z Deckiem",
	allGames: "Wszystkie gry",
	installed: "Zainstalowane",
	nonSteam: "Spoza Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$8 = {
	error: "Błąd Unifideck",
	errorLoadingView: "Nie udało się załadować zunifikowanego widoku biblioteki",
	checkConsole: "Sprawdź konsolę przeglądarki w celu poznania szczegółów",
	errorLoadingGames: "Błąd ładowania gier",
	tryReloading: "Spróbuj przeładować wtyczkę lub sprawdź konsolę",
	storeLabel: "Sklep:",
	allStores: "Wszystkie sklepy",
	searchPlaceholder: "Szukaj gier...",
	library: "Biblioteka"
};
var gameInfoPanel$8 = {
	loading: "Ładowanie informacji o grze...",
	noMetadata: "Brak metadanych dla tej gry.",
	noTestResults: "Brak szczegółowych wyników testów.",
	buttons: {
		details: "Szczegóły",
		synopsis: "Streszczenie",
		storePage: "Strona sklepu",
		dlc: "DLC",
		communityHub: "Centrum społeczności",
		pointsShop: "Sklep z punktami",
		discussions: "Dyskusje",
		guides: "Poradniki",
		support: "Wsparcie",
		install: "Zainstaluj",
		uninstall: "Odinstaluj",
		cancel: "Anuluj",
		achievements: "Osiągnięcia"
	},
	labels: {
		store: "Sklep:",
		name: "Nazwa:",
		size: "Rozmiar:",
		developer: "Deweloper:",
		publisher: "Wydawca:",
		released: "Wydano:",
		metacritic: "Metacritic:",
		status: "Status:",
		spaceRequired: "Wymagane miejsce",
		lastPlayed: "Ostatnio grano",
		playTime: "Czas gry",
		never: "Nigdy"
	},
	compatibility: {
		modalTitle: "Zgodność ze Steam Deckiem",
		viewOnProtonDb: "Zobacz na ProtonDB",
		close: "Zamknij",
		verified: "ZWERYFIKOWANA",
		playable: "GRYWALNA",
		unsupported: "NIEWSPIERANA",
		unknown: "NIEZNANA"
	},
	failedToLoadMetadata: "Nie udało się załadować metadanych",
	lastSession: "Ostatnia sesja: odblokowano {{names}}"
};
var gameDetailsSettings$8 = {
	title: "WIDOK SZCZEGÓŁÓW GRY",
	simple: "Prosty",
	detailed: "Szczegółowy"
};
var gogLanguageModal$8 = {
	title: "Wybierz język",
	description: "Wybierz język pobierania dla {{title}}",
	label: "Język",
	install: "Zainstaluj",
	cancel: "Anuluj"
};
var accountSwitch$8 = {
	title: "Wykryto zmianę konta",
	description: "Zalogowano się na inne konto Steam. Co chcesz zrobić z danymi Unifideck?",
	migrate: "Migruj dane Unifideck",
	migrateDescription: "Kopiuj gry i okładki z poprzedniego konta",
	freshStart: "Czysty start",
	freshStartDescription: "Wyczyść dane logowania sklepów, aby połączyć inne konta Epic/GOG/Amazon/Ubisoft",
	skip: "Pomiń",
	toastMigrateTitle: "Migracja zakończona",
	toastMigrateBody: "Zmigrowano {{shortcuts}} skrótów i {{artwork}} plików okładek",
	toastClearTitle: "Wyczyszczono tokeny uwierzytelniania",
	toastClearBody: "Dane logowania sklepów zostały wyczyszczone. Możesz teraz zalogować się na inne konta."
};
var ubisoftAuth$8 = {
	title: "Logowanie Ubisoft Connect",
	description: "Zaloguj się przez Ubisoft Connect, aby połączyć swoją bibliotekę.",
	signInViaUpc: "Zaloguj się przez Ubisoft Connect",
	upcDescription: "Ubisoft Connect zostanie otwarty w celu zalogowania. Po zalogowaniu Twoja sesja zostanie przechwycona automatycznie.",
	launchingUpc: "Uruchamianie Ubisoft Connect...",
	waitingForLogin: "Oczekiwanie na zalogowanie...",
	retryLaunch: "Spróbuj ponownie",
	cancel: "Anuluj",
	launchFailed: "Nie udało się uruchomić Ubisoft Connect. Spróbuj ponownie.",
	signingIn: "Logowanie...",
	emailLabel: "E-mail",
	passwordLabel: "Hasło",
	signIn: "Zaloguj się",
	twoFactorTitle: "Uwierzytelnianie dwuskładnikowe",
	twoFactorDescription: "Wprowadź kod weryfikacyjny z aplikacji uwierzytelniającej.",
	twoFactorLabel: "Kod weryfikacyjny",
	verify: "Zweryfikuj",
	verifying: "Weryfikowanie...",
	errorEmptyFields: "Wprowadź e-mail i hasło",
	errorEmpty2FA: "Wprowadź kod weryfikacyjny",
	errorLoginFailed: "Logowanie nie powiodło się. Sprawdź swoje dane uwierzytelniające.",
	errorInvalid2FA: "Nieprawidłowy kod weryfikacyjny. Spróbuj ponownie.",
	showPassword: "Pokaż hasło",
	hidePassword: "Ukryj hasło",
	doubleLoginWarning: "Musisz zalogować się dwukrotnie do Ubisoft."
};
var relativeDate$8 = {
	today: "Dzisiaj",
	yesterday: "Wczoraj"
};
var common$8 = {
	thisGame: "tę grę",
	game: "Gra",
	unknown: "Nieznane",
	ok: "OK",
	cancel: "Anuluj",
	working: "Przetwarzanie…",
	loading: "Ładowanie…",
	free: "darmowa",
	close: "Zamknij"
};
var info$8 = {
	expandDetails: "Pokaż szczegóły",
	collapseDetails: "Ukryj szczegóły"
};
var microsoft$8 = {
	notCompatible: "Ten format gry nie jest zgodny ze SteamOS",
	playOnCloud: "Graj w chmurze",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Zaloguj się na swoje konto Microsoft / Xbox",
	accountConnected: "Połączono z kontem Microsoft",
	loggedOut: "Wylogowano ze sklepu Microsoft Store",
	xcloudNotInstalled: "Gry xCloud nie są instalowane lokalnie",
	chromiumRequired: "Wymagany Microsoft Edge",
	chromiumRequiredMessage: "Logowanie wymaga przeglądarki Microsoft Edge. Czy chcesz zainstalować ją automatycznie? (Wymagane tylko raz).",
	chromiumInstallButton: "Zainstaluj Microsoft Edge",
	chromiumInstalling: "Instalowanie Microsoft Edge...",
	chromiumInstalled: "Zainstalowano Microsoft Edge!",
	chromiumAlreadyInstalled: "Zgodna przeglądarka jest już zainstalowana",
	chromiumInstallFailed: "Nie udało się zainstalować Microsoft Edge. Zainstaluj ręcznie: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Przekroczono limit czasu instalacji Microsoft Edge. Sprawdź połączenie z internetem.",
	browserAlreadyInstalled: "Zgodna przeglądarka jest już zainstalowana",
	browserInstalled: "Microsoft Edge został pomyślnie zainstalowany!",
	browserInstallFailed: "Nie udało się zainstalować Microsoft Edge. Zainstaluj ręcznie: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak nie jest dostępny w tym systemie",
	signInRequired: "Zaloguj się, aby zagrać",
	syncSkippedNoSubscription: "Nie wykryto aktywnej subskrypcji Xbox Game Pass. Twoja biblioteka xCloud nie została zsynchronizowana.",
	syncSkippedTierUnknown: "Wykryto subskrypcję Xbox Game Pass, ale nie można określić poziomu subskrypcji. Pominięto synchronizację, aby zapobiec wyświetlaniu niedostępnych gier.",
	subscriptionCheckFailed: "Nie można zweryfikować subskrypcji Xbox Game Pass. Pominięto synchronizację xCloud. Spróbuj ponownie, gdy połączenie będzie stabilne.",
	subscriptionDetected: "Aktywna subskrypcja Xbox Game Pass {{tier}}."
};
var playButton$8 = {
	play: "Graj",
	install: "Zainstaluj",
	cancel: "Anuluj",
	retry: "Ponów",
	downloadFailed: "Pobieranie nie powiodło się",
	installing: "Instalowanie",
	controllerConfig: "Konfiguracja kontrolera",
	appSettings: "Ustawienia aplikacji"
};
var play$8 = {
	play: "Graj",
	resume: "Wznów",
	stop: "Zatrzymaj",
	cancel: "Anuluj",
	cancelling: "Anulowanie…",
	update: "Aktualizuj",
	uninstall: "Odinstaluj",
	queued: "W kolejce",
	downloading: "Pobieranie",
	extracting: "Rozpakowywanie",
	verifying: "Weryfikowanie",
	finalizing: "Finalizowanie instalacji",
	cancelConfirmTitle: "Anulować pobieranie?",
	cancelConfirmBody: "Czy na pewno chcesz anulować pobieranie gry {{title}}? Częściowo pobrane pliki zostaną usunięte.",
	cancelConfirmConfirm: "Anuluj pobieranie",
	cancelConfirmCancel: "Kontynuuj pobieranie",
	playOnCloud: "Graj w chmurze",
	cloudSave: {
		autoPullOn: "Zapisy w chmurze są pobierane automatycznie przy uruchomieniu.",
		close: "Zamknij",
		download: "Pobierz zapis z chmury",
		filesCount: "{{count}} plików",
		label: "Zapisy w chmurze",
		menuTitle: "Zapisy w chmurze — {{game}}",
		noCloudData: "Nieznana",
		noCloudSupport: "Brak obsługi zapisów w chmurze",
		setLocation: "Ustaw lokalizację zapisu",
		setLocationHint: "Wybierz folder, w którym ta gra przechowuje swoje zapisy.",
		setLocationTitle: "Wybierz folder zapisu tej gry",
		statusCloudAvailable: "Zapis w chmurze dostępny",
		statusNoSaves: "Brak lokalnych zapisów",
		statusSyncing: "Synchronizowanie zapisów w chmurze…",
		statusUnresolved: "Nie znaleziono lokalizacji zapisu — synchronizacja może nie działać dla tej gry.",
		unresolvedHint: "Uruchom grę raz, abyśmy mogli wykryć jej folder zapisu, lub ustaw lokalizację ręcznie.",
		upload: "Prześlij lokalny zapis"
	},
	exe: {
		browse: "Przeglądaj…",
		changeFailed: "Nie można zmienić pliku wykonywalnego",
		changed: "Zmieniono plik wykonywalny",
		defaultTag: "(domyślny)",
		menuItem: "Zmień plik wykonywalny…",
		reset: "Przywróć domyślny plik wykonywalny",
		resetButton: "Przywróć domyślne",
		subtitle: "Wybierz plik uruchamiający tę grę (np. pomiń launcher i uruchom grę bezpośrednio). Katalog roboczy oraz zapisy w chmurze nie zostaną naruszone.",
		title: "Zmień plik wykonywalny — {{game}}",
		unavailable: "Nie można odczytać folderu tej gry."
	}
};
var playMeta$8 = {
	spaceRequired: "Wymagane miejsce",
	installedSize: "Zajmowane miejsce",
	lastPlayed: "Ostatnio grano",
	neverPlayed: "Nigdy nie grano",
	hoursShort: "godz.",
	minutesShort: "min.",
	played: "Rozegrano"
};
var downloads$8 = {
	title: "Pobieranie",
	empty: "Brak pobrań. Zainstaluj gry ze swojej biblioteki, aby zobaczyć je tutaj.",
	current: "Pobieranie w toku",
	queued: "W kolejce",
	finished: "Ostatnio ukończone",
	cancel: "Anuluj",
	removeFromQueue: "Usuń",
	play: "Graj",
	outcome: {
		queued: "W kolejce",
		downloading: "Pobieranie",
		completed: "Ukończono",
		cancelled: "Anulowano",
		error: "Nie powiodło się"
	}
};
var navigation$8 = {
	loading: "Ładowanie...",
	storesTitle: "Sklepy",
	syncTitle: "Sync biblioteki",
	languageTitle: "Język"
};
var security$8 = {
	panelTitle: "Bezpieczeństwo",
	counters: {
		label: "Liczniki",
		empty: "Brak zaobserwowanych zdarzeń od momentu uruchomienia"
	},
	log: {
		label: "Dziennik inspekcji",
		entryCount: "{{count}} wpis",
		entryCount_other: "{{count}} wpisy",
		emptyFiltered: "Brak zdarzeń pasujących do filtra"
	},
	filter: {
		label: "Filtr",
		all: "Wszystkie zdarzenia"
	},
	timeline: {
		label: "Ostatnie 24 godziny ({{count}} zdarzenie)",
		label_other: "Ostatnie 24 godziny ({{count}} zdarzenia)",
		axisStart: "-24 godz.",
		axisEnd: "teraz"
	},
	relativeTime: {
		seconds: "{{count}} s temu",
		minutes: "{{count}} min temu",
		hours: "{{count}} godz. temu",
		days: "{{count}} dni temu"
	},
	bruteforce: {
		warningTitle: "Ostrzeżenie: Wykryto próbę brute-force (poziom ostrzegawczy)",
		escalationTitle: "ALARM: Wykryto próbę brute-force (poziom eskalacji)",
		detail: "{{failures}} nieudanych prób odszyfrowania w ciągu ostatnich {{window}} s. Progi: {{warning}} (ostrzeżenie) / {{escalation}} (eskalacja).",
		resetButton: "Potwierdź i zresetuj wykrywacz"
	},
	actions: {
		clearLog: "Wyczyść dziennik i liczniki"
	},
	events: {
		tokenEncrypted: "Zaszyfrowano token",
		tokenDecrypted: "Odszyfrowano token",
		decryptFailed: "Odszyfrowywanie nie powiodło się",
		tokenFileMigrated: "Zmigrowano plik tokenów",
		legacyPlaintextDetected: "Wykryto przestarzały zwykły tekst",
		authFlowStarted: "Rozpoczęto uwierzytelnianie",
		authFlowCompleted: "Ukończono uwierzytelnianie",
		authFlowFailed: "Uwierzytelnianie nie powiodło się",
		permissionsCheck: "Sprawdzono uprawnienia",
		permissionsRepaired: "Naprawiono uprawnienia",
		bruteforceSuspected: "Podejrzenie próby brute-force",
		deviceResetDetected: "Wykryto reset urządzenia",
		fingerprintInitialized: "Zainicjowano odcisk palca",
		externalAuthCheckFailed: "Weryfikacja zewnętrznego uwierzytelniania nie powiodła się",
		configValidationCompleted: "Ukończono walidację konfiguracji",
		configValidationFailed: "Walidacja konfiguracji nie powiodła się"
	},
	configValidation: {
		degradedTitle: "Nieprawidłowa konfiguracja: tryb ograniczony jest aktywny",
		detail: "Wykryto {{count}} błąd walidacji podczas uruchamiania",
		detail_other: "Wykryto {{count}} błędy walidacji podczas uruchamiania",
		sourceDefaults: "wartości domyślne",
		sourceUser: "ustawienia użytkownika",
		sourceMerged: "scalone",
		overflow: "+ {{count}} inny błąd",
		overflow_other: "+ {{count}} inne błędy"
	}
};
var cloudSyncBehavior$8 = {
	_comment: "Panel ustawień, który pozwala użytkownikowi wybrać, dla każdego sklepu, sposób wyświetlania błędów synchronizacji zapisów w chmurze. Pięć wpisów store_label odzwierciedla wiersze renderowane przez CloudSyncBehaviorPanel.tsx. Microsoft jest celowo pominięty: przesyłanie strumieniowe xCloud nie ma lokalnego zapisu do synchronizacji.",
	title: "Zachowanie synchronizacji w chmurze",
	description: "Wybierz, jak Unifideck powinien reagować, gdy pobieranie lub przesyłanie zapisu w chmurze nie powiedzie się podczas uruchamiania gry.",
	loadError: "Nie udało się załadować bieżących ustawień",
	saveError: "Nie udało się zapisać zmiany. Spróbuj ponownie",
	store_label: {
		"default": "Domyślnie (wszystkie sklepy)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Cichy (tylko w dzienniku)",
		toast: "Powiadom (komunikat wyskakujący)"
	}
};
var cloudSync$8 = {
	_comment: "Komunikaty wyskakujące generowane przez program uruchamiający, gdy synchronizacja w chmurze nie powiedzie się. Zastępczy błąd jest rozwiązywany przez cloudSync.error.<code> przy użyciu pola error_i18n_key z ładunku backendu.",
	error: {
		network_unreachable: "Sieć niedostępna",
		auth_expired: "Uwierzytelnianie wygasło",
		forbidden: "Dostęp zabroniony",
		quota_exceeded: "Przekroczono limit pamięci w chmurze",
		server_error: "Serwer sklepu niedostępny",
		timed_out: "Upłynął limit czasu żądania",
		disk_full: "Brak miejsca na dysku",
		disk_space_low: "Pominięto: Mało miejsca na dysku (zwolnij miejsce i spróbuj ponownie)",
		permission_denied: "Brak uprawnień",
		cancelled: "Anulowano",
		unknown: "Nieoczekiwany błąd"
	},
	unavailableNativeDep: "Zapisy w chmurze są wyłączone na tym systemie — w jego Pythonie ({{python}}) brakuje wymaganego komponentu. Twoje lokalne zapisy nie zostaną naruszone; przebuduj/przeinstaluj wtyczkę po aktualizacji systemu, aby przywrócić synchronizację."
};
var launchLogs$8 = {
	_comment: "Wiadomości dla LaunchLogsModal. Dziennik inspekcji jest otwierany z akcji powiadomienia unifideck://show-logs/<launch_id>, dołączonej do ostrzeżeń o błędach launchera (circuit breaker + LauncherError). Odczytuje zarchiwizowane logi dla każdego uruchomienia w ~/.local/share/unifideck/launches/<launch_id>.log i wyświetla je pokolorowane według ważności.",
	title: "Logi uruchamiania — {{launchId}}",
	close: "Zamknij",
	copyAll: "Skopiuj wszystko",
	"export": "Eksportuj do pliku",
	copied: "Skopiowano do schowka",
	copyFailed: "Kopiowanie nie powiodło się",
	exporting: "Eksportowanie…",
	exportedTo: "Wyeksportowano do {{path}}",
	exportFailed: "Błąd eksportu: {{error}}",
	loading: "Ładowanie logów…",
	notAvailable: "Logi tego uruchomienia nie są już dostępne (poza 7-dniowym okresem przechowywania).",
	error: "Nie można odczytać logów: {{error}}",
	fileInfo: "{{path}} — wyświetlanie {{shown}} z {{total}} linii",
	filterLabel: "Wyświetl",
	filterInfo: "Informacje",
	filterWarning: "Ostrzeżenie",
	filterError: "Błąd",
	nextError: "Następny błąd ({{current}}/{{total}})",
	noErrors: "Brak błędów",
	allFiltered: "Wszystkie linie zostały ukryte przez bieżący filtr. Włącz co najmniej jeden poziom ważności, aby zobaczyć wpisy."
};
var settings$8 = {
	_comment: "Kontrolki działań UnifideckSettingsPanel, które nie są obsługiwane przez bardziej szczegółowe komponenty (LanguageSelector, CloudSyncBehaviorPanel). refreshAll to przycisk wyzwalający sync_all() na wszystkich zarejestrowanych sklepach; widok biblioteki wyświetla postęp synchronizacji poprzez własną subskrypcję zdarzeń SYNC_PROGRESS.",
	storeConnections: "Połączenia sklepów",
	librarySync: "Sync biblioteki",
	language: "Język",
	languageAuto: "Wykryj automatycznie",
	refreshAll: {
		sectionTitle: "Działania biblioteki",
		button: "Odśwież wszystkie biblioteki",
		buttonBusy: "Uruchamianie…",
		startedTitle: "Rozpoczęto odświeżanie biblioteki",
		startedBody: "Synchronizacja wszystkich sklepów w tle.",
		startedSingle: "Odświeżanie biblioteki {{store}} w tle.",
		failedTitle: "Nie udało się rozpocząć odświeżania biblioteki"
	}
};
var saveFolder$8 = {
	_comment: "Wiadomości dla SaveFolderModal. Modal jest otwierany z akcji powiadomienia unifideck://open-save-folder/<store>/<game_id>, zwykle wyświetlanej przy błędach synchronizacji w chmurze, gdy użytkownik chce sprawdzić, co jest zapisane lokalnie. pola count i size są interpolowane; truncated jest wyświetlane tylko wtedy, gdy wynik został ograniczony.",
	title: "Folder zapisu — {{store}} / {{gameId}}",
	close: "Zamknij",
	copyPath: "Kopiuj ścieżkę",
	copyFileList: "Kopiuj listę plików",
	depthLabel: "Głębokość",
	filterLabel: "Filtr",
	loading: "Ładowanie zawartości folderu…",
	noSavesYet: "Nie znaleziono jeszcze lokalnych zapisów dla tej gry. Folder zostanie utworzony przy następnej udanej synchronizacji w chmurze.",
	aggregate: "{{count}} plików, łącznie {{size}}",
	truncated: "Dodatkowe {{count}} plików ({{size}}) nie zostało wyświetlonych. Zawęź filtr, aby je zobaczyć.",
	error: "Nie można odczytać folderu zapisu: {{error}}"
};
var library$8 = {
	_comment: "Usługi UI dla poszczególnych gier w widoku biblioteki, w tym wskaźnik bezpiecznika uruchamiania (circuit breaker) oraz kontrolki wymuszenia uruchomienia / resetu przy powtarzających się błędach.",
	circuitBreaker: {
		_comment: "Interfejs dla bezpiecznika błędów uruchamiania. Wskaźnik pojawia się tylko wtedy, gdy liczba błędów w skonfigurowanym oknie czasowym osiągnie próg circuit_breaker.failures_threshold (domyślnie 3 w ciągu 600 s). Wymuszenie uruchomienia powoduje jednorazowe pominięcie zabezpieczenia; Resetowanie czyści całą historię błędów.",
		badgeRecent: "{{count}} niedawny błąd uruchomienia",
		badgeRecent_other: "{{count}} niedawne błędy uruchomienia",
		badgeTooltip: "{{count}} nieudanych uruchomień w ostatnim oknie czasowym. Bezpiecznik wstrzymał automatyczne uruchamianie.",
		forceLaunchButton: "Wymuś uruchomienie",
		resetButton: "Resetuj błędy",
		failureKind: {
			fastBoot: "Awarie szybkiego startu",
			launcherError: "Błąd przygotowania launchera"
		}
	}
};
var pickStorage$8 = {
	title: "Lokalizacja instalacji",
	customOption: "Wybierz inną lokalizację...",
	customLabel: "Niestandardowa lokalizacja",
	change: "Zmień...",
	gameSize: "Rozmiar instalacji"
};
var restart$8 = {
	title: "Synchronizacja zakończona!",
	body: "Uruchom ponownie Steam, aby zastosować nowe gry i okładki",
	confirm: "Uruchom ponownie teraz",
	later: "Później"
};
var updater$8 = {
	checkButton: "Sprawdź aktualizacje",
	checkCompleteMessage: "Pomyślnie pobrano informacje o najnowszej wersji.",
	checkCompleteTitle: "Sprawdzanie aktualizacji zakończone",
	checkFailedTitle: "Sprawdzanie nie powiodło się",
	checkingButton: "Sprawdzanie...",
	currentTitle: "Aktualna",
	downgradeButton: "Downgrade do v{{version}}",
	installFailedTitle: "Instalacja nie powiodła się",
	installTimeoutMessage: "Brak odpowiedzi od Decky Loader — instalacja mogła się zawiesić lub nie powieść. Spróbuj ponownie.",
	installedLabel: "zainstalowano",
	installingButton: "Instalowanie...",
	installingTitle: "Instalowanie wtyczki",
	latestLabel: "najnowsza",
	modalTitle: "UNIFIDECK v{{version}} — Lista zmian",
	noBackend: "Backend Decky jest niedostępny.",
	noReleaseNotes: "Brak listy zmian.",
	reinstallButton: "Zainstaluj ponownie v{{version}}",
	refreshFailedMessage: "Nie udało się zweryfikować informacji o najnowszej wersji. Sprawdź połączenie i spróbuj ponownie.",
	releaseGoneMessage: "Ta wersja nie jest już dostępna. Sprawdź aktualizacje i wybierz ją ponownie.",
	releaseNotesButton: "Lista zmian",
	stageDownload: "Pobieranie…",
	stageFinish: "Kończenie…",
	stageInstall: "Instalowanie…",
	stageOpen: "Odczytywanie pakietu…",
	stageParse: "Weryfikowanie…",
	stageRemove: "Usuwanie starej wersji…",
	stageStart: "Uruchamianie…",
	stageWorking: "Instalowanie…",
	titleLoading: "Sprawdzanie wersji",
	typeDowngrade: "Downgradowanie do",
	typeReinstall: "Instalowanie ponowne",
	typeUpdate: "Aktualizowanie do",
	updateButton: "Aktualizuj do v{{version}}"
};
var achievements$8 = {
	unlockedTitle: "Osiągnięcie odblokowane",
	unlockedBody: "{{name}}"
};
var achievementsModal$8 = {
	title: "Osiągnięcia",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Odśwież",
	close: "Zamknij",
	loading: "Ładowanie osiągnięć…",
	empty: "Ta gra nie ma osiągnięć.",
	hidden: "Ukryte osiągnięcie",
	unlockedOn: "Odblokowano {{date}}",
	error: "Nie udało się wczytać osiągnięć.",
	errorOffline: "Połącz się z internetem, aby zobaczyć osiągnięcia.",
	errorAuth: "Zaloguj się w sklepie tej gry, aby zobaczyć osiągnięcia.",
	errorUnavailable: "Osiągnięcia nie są dostępne dla tej gry."
};
var collectionSettings$8 = {
	title: "KOLEKCJE STEAM",
	enabled: "Kolekcje włączone",
	disabled: "Kolekcje wyłączone"
};
var plPL = {
	_comment: _comment$8,
	tabs: tabs$8,
	auth: auth$8,
	storage: storage$8,
	storageSettings: storageSettings$8,
	gameGrid: gameGrid$8,
	downloadsTab: downloadsTab$8,
	cloudSave: cloudSave$8,
	storeConnections: storeConnections$8,
	librarySync: librarySync$8,
	cleanup: cleanup$8,
	captureLogs: captureLogs$8,
	installButton: installButton$8,
	confirmModals: confirmModals$8,
	toasts: toasts$8,
	uninstallModal: uninstallModal$8,
	languageSettings: languageSettings$8,
	errors: errors$8,
	sync: sync$8,
	force_sync: force_sync$8,
	artwork: artwork$8,
	deckTabs: deckTabs$8,
	unifiedLibrary: unifiedLibrary$8,
	gameInfoPanel: gameInfoPanel$8,
	gameDetailsSettings: gameDetailsSettings$8,
	gogLanguageModal: gogLanguageModal$8,
	accountSwitch: accountSwitch$8,
	ubisoftAuth: ubisoftAuth$8,
	relativeDate: relativeDate$8,
	common: common$8,
	info: info$8,
	microsoft: microsoft$8,
	playButton: playButton$8,
	play: play$8,
	playMeta: playMeta$8,
	downloads: downloads$8,
	navigation: navigation$8,
	security: security$8,
	cloudSyncBehavior: cloudSyncBehavior$8,
	cloudSync: cloudSync$8,
	launchLogs: launchLogs$8,
	settings: settings$8,
	saveFolder: saveFolder$8,
	library: library$8,
	pickStorage: pickStorage$8,
	restart: restart$8,
	updater: updater$8,
	achievements: achievements$8,
	achievementsModal: achievementsModal$8,
	collectionSettings: collectionSettings$8
};

var _comment$7 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$7 = {
	settings: "Настройки",
	downloads: "Загрузки"
};
var auth$7 = {
	connect: "Войти",
	disconnect: "Выйти",
	status: {
		connected: "Подключено",
		disconnected: "Не подключено",
		checking: "Проверка…",
		error: "Ошибка — проверьте логи"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$7 = {
	title: "Хранилище",
	defaultUpdated: "Хранилище по умолчанию обновлено",
	isDefault: "По умолчанию",
	setDefault: "Сделать по умолчанию",
	customPath: "Свой путь",
	up: "↑ Наверх",
	refresh: "Обновить",
	useThisPath: "Использовать этот путь"
};
var storageSettings$7 = {
	title: "Настройки загрузки",
	installLocationLabel: "Папка установки",
	installLocationDescription: "Куда будут скачиваться новые игры",
	pathLabel: "Путь",
	loading: "Загрузка параметров хранилища...",
	toastUpdatedTitle: "Папка установки обновлена",
	toastUpdatedBody: "Новые игры будут устанавливаться в {{location}}",
	toastFailedTitle: "Не удалось обновить",
	toastFailedBody: "{{error}}",
	unknown: "Неизвестно",
	browseButton: "Обзор...",
	clearCustom: "Сбросить путь",
	customPathSet: "Установлена пользовательская папка установки",
	newFolder: "Новая папка",
	newFolderCreate: "Создать",
	selectFolder: "Выбрать эту папку",
	emptyDirectory: "Пустая папка",
	showHidden: "Показывать скрытые",
	selectDevice: "Выбрать накопитель",
	sortAZ: "А-Я",
	sortZA: "Я-А",
	sortModifiedNewest: "Изменено (сначала новые)",
	sortModifiedOldest: "Изменено (сначала старые)",
	sortCreatedNewest: "Создано (сначала новые)",
	sortCreatedOldest: "Создано (сначала старые)",
	sortSizeLargest: "Размер (по убыванию)",
	sortSizeSmallest: "Размер (по возрастанию)",
	"validationErrors.notAbsolute": "Путь должен быть абсолютным",
	"validationErrors.systemDir": "Нельзя использовать системную папку",
	"validationErrors.notWritable": "В эту папку нет прав на запись",
	"validationErrors.notFound": "Папка не существует",
	"validationErrors.homeRoot": "Нельзя использовать корневую домашнюю папку",
	freeSpaceLabel: "{{label}} ({{freeSpace}} ГБ свободно)"
};
var gameGrid$7 = {
	loading: "Загрузка игр...",
	noGamesFound: "Игры не найдены",
	noGamesHint: "Попробуйте синхронизировать библиотеки в настройках Unifideck",
	showingGames: "Показано игр: {{count}}",
	installed: "Установлено",
	notInstalled: "Не установлено",
	playtime: "Сыграно: {{hours}} ч. {{minutes}} мин."
};
var downloadsTab$7 = {
	currentDownload: "ТЕКУЩАЯ ЗАГРУЗКА",
	queuedDownloads: "В ОЧЕРЕДИ ({{count}})",
	recentlyCompleted: "НЕДАВНО ЗАВЕРШЕННЫЕ",
	noActiveDownloads: "Нет активных загрузок",
	noDownloads: "Загрузок нет. Установите игры из своей библиотеки, чтобы увидеть их здесь.",
	loadingLabel: "Загрузка...",
	loadingMessage: "Получение очереди загрузок...",
	confirmCancelTitle: "Подтверждение отмены",
	confirmCancelDescription: "Вы уверены, что хотите отменить загрузку {{game}}?",
	confirmCancelYes: "Да",
	confirmCancelNo: "Нет",
	cancelDownload: "Отмена",
	toastDownloadCancelledTitle: "Загрузка отменена",
	toastDownloadCancelledBody: "Загрузка была удалена из очереди.",
	toastCancelFailedTitle: "Не удалось отменить",
	toastCancelFailedBody: "Не удалось отменить загрузку: {{error}}",
	status: {
		queued: "В очереди",
		downloading: "Загрузка",
		completed: "Завершено",
		cancelled: "Отменено",
		error: "Ошибка"
	},
	preparingDownload: "Подготовка к загрузке...",
	phaseExtracting: "Распаковка {{game}}...",
	phaseVerifying: "Проверка {{game}}...",
	internalStorage: "Внутренняя память ({{freeSpace}} ГБ свободно)",
	sdCard: "Карта SD ({{freeSpace}} ГБ свободно)",
	customLocation: "Другая папка: {{path}} ({{freeSpace}} ГБ свободно)",
	extractingLabel: "Распаковка...",
	verifyingLabel: "Проверка...",
	updateQueuedLabel: "Обновление в очереди",
	downloadQueuedLabel: "Загрузка в очереди",
	downloadingUpdateLabel: "Загрузка обновления",
	downloadingLabel: "Загрузка...",
	finalizingInstallation: "Завершение установки...",
	speedMbps: "{{speed}} МБ/с",
	etaLabel: "Осталось времени: {{eta}}",
	preparingMessage: "Инициализация префикса и синхронизация облачных сохранений...",
	preparingLabel: "Настройка игры...",
	installingViaUpcLabel: "Установка через Ubisoft Connect",
	extractingMessage: "Извлечение файлов...",
	verifyingMessage: "Проверка файлов... {{pct}}%",
	installingViaUpcMessage: "Следуйте инструкциям в окне Ubisoft Connect, чтобы завершить установку..."
};
var cloudSave$7 = {
	title: "Конфликт облачных сохранений",
	description: "Локальные сохранения для {{game}} отличаются от облачных",
	local: "Локальное:",
	cloud: "Облачное:",
	newer: "(новее)",
	useCloud: "Использовать облачное",
	useLocal: "Использовать локальное",
	unknownDate: "Неизвестно",
	toastUsingCloudTitle: "Использование облачных сохранений",
	toastUsingCloudBody: "Скачивание облачных сохранений для {{game}}",
	toastUsingLocalTitle: "Использование локальных сохранений",
	toastUsingLocalBody: "Загрузка локальных сохранений в облако для {{game}}",
	toastErrorTitle: "Ошибка",
	toastErrorBody: "Не удалось разрешить конфликт сохранений",
	filesSize: "{{count}} файлов · {{size}}",
	uploadBlockedEmpty: "Загрузка в облако для {{game}} пропущена — локальные сохранения не найдены",
	uploadSkippedTitle: "Загрузка в облако пропущена",
	uploadSkippedBody: "Пока нет локальных сохранений для загрузки для {{game}}."
};
var storeConnections$7 = {
	title: "ПОДКЛЮЧЕНИЕ МАГАЗИНОВ",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Подключено",
	checking: "Проверка...",
	authenticate: "Войти",
	legendaryNotInstalled: "⚠️ Установка утилиты Epic Games...",
	nileNotInstalled: "⚠️ Утилита Amazon Games не найдена",
	error: "Ошибка — проверьте журналы"
};
var librarySync$7 = {
	title: "СИНХРОНИЗАЦИЯ БИБЛИОТЕКИ",
	syncLibraries: "Синхронизировать библиотеки",
	forceSync: "Принудительная синхронизация",
	syncing: "Синхронизация...",
	cancelSync: "Отменить синхронизацию",
	retryStatusCheck: "Повторить проверку статуса",
	gamesSynced: "Синхронизировано игр: {{synced}} / {{total}}",
	artworkDownloaded: "Скачано обложек: {{synced}} / {{total}}",
	metadataExtracted: "Извлечено метаданных: {{synced}} / {{total}}",
	steamMetadataDownloaded: "Метаданные Steam: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "Метаданные unifiDB: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Данные Metacritic: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "Метаданные RAWG: {{synced}} / {{total}}",
	compatChecked: "Данные совместимости: {{synced}} / {{total}}",
	cancelling: "Отмена…",
	error: "Ошибка"
};
var cleanup$7 = {
	title: "Очистка",
	deleteAll: "Удалить все библиотеки и кэш UNIFIDECK",
	modalTitle: "Удалить все данные Unifideck?",
	modalDescription: "Это удалит все ярлыки Unifideck, обложки Steam, сохраненные авторизации магазинов и данные кэша. Установленные игры останутся на диске и могут быть повторно синхронизированы. Включите разрушительный режим ниже, чтобы также удалить файлы установленных игр.",
	deleteFilesLabel: "Также удалить файлы установленных игр (Разрушительно)",
	deleteFilesDescription: "Удаляет папку установки на диске для каждой игры Unifideck в безопасном корневом каталоге.",
	confirmDelete: "Да, удалить все",
	cancel: "Отмена",
	deleting: "Удаление..."
};
var captureLogs$7 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (ru-RU).",
	title: "Сбор журналов",
	description: "Собирает все журналы Unifideck в один ZIP-архив в папке «Загрузки», чтобы вы могли приложить его к сообщению об ошибке.",
	capture: "Собрать журналы",
	capturing: "Сбор журналов...",
	savedTo: "Сохранено в папку «Загрузки»",
	bundleInfo: "{{files}} файлов · {{size}}",
	partialNote: "Не удалось прочитать {{count}} файл. Архив всё равно пригоден.",
	partialNote_other: "Не удалось прочитать {{count}} файлов. Архив всё равно пригоден.",
	copyPath: "Копировать путь",
	modalTitle: "Создать диагностический архив?",
	modalDescription: "Unifideck соберёт свои журналы в один ZIP-файл в папке «Загрузки», чтобы вы могли приложить его к сообщению об ошибке.",
	modalIncludes: "Включено: журналы плагина и запусков игр, история синхронизации и установок, названия ваших игр и пути установки, а также сведения об устройстве — модель, операционная система и накопители.",
	modalExcludes: "Никогда не включается: ваши пароли, токены входа в магазины и файлы cookie браузера.",
	modalConfirm: "Создать архив",
	cancel: "Отмена"
};
var installButton$7 = {
	play: "Играть",
	resume: "Продолжить",
	install: "Установить {{title}}",
	installNative: "Установить",
	uninstall: "Удалить {{title}}",
	cancel: "✖ Отмена",
	processing: "Обработка...",
	checking: "Проверка...",
	size: "{{size}} ГБ",
	update: "ОБНОВИТЬ",
	updateAvailable: "ДОСТУПНО ОБНОВЛЕНИЕ",
	updateAvailableDescription: "Новая версия готова к установке."
};
var confirmModals$7 = {
	installTitle: "Подтверждение установки",
	installDescription: "Вы уверены, что хотите установить {{title}}?",
	uninstallTitle: "Подтверждение удаления",
	uninstallDescription: "Вы уверены, что хотите удалить {{title}}?",
	cancelTitle: "Подтверждение отмены",
	cancelDescription: "Вы уверены, что хотите отменить загрузку {{title}}?",
	forceSyncTitle: "Параметры принудительной синхронизации",
	forceSyncDescription: "Вы хотите заново синхронизировать все обложки? Это перезапишет любые ваши ручные изменения обложек. Выберите 'Сохранить обложки', чтобы скачать только недостающие.",
	resyncArtwork: "Перезаписать обложки",
	keepArtwork: "Сохранить обложки",
	steamRestartTitle: "Синхронизация завершена!",
	steamRestartDescription: "Пожалуйста, перезапустите Steam, чтобы новые игры и обложки появились в библиотеке",
	steamRestartDescriptionStore: "Пожалуйста, перезапустите Steam, чтобы новые игры и обложки {{store}} появились в библиотеке",
	steamRestartCleanupTitle: "Очистка завершена!",
	steamRestartCleanupDescription: "Пожалуйста, перезапустите Steam, чтобы полностью убрать удаленные игры и обложки",
	restartNow: "Перезапустить сейчас",
	later: "Позже",
	ubisoftInstallTitle: "Требуется Ubisoft Connect",
	ubisoftInstallDescription: "Это запустит программу установки Ubisoft Connect, которая необходима для работы {{title}}.",
	ubisoftInstallConfirm: "Открыть Ubisoft Connect",
	yes: "Да",
	no: "Нет",
	amazonUpdateTitle: "Требуется полная перегрузка",
	amazonUpdateDescription: "Обновления Amazon требуют полной повторной загрузки игры {{title}}. Это может занять некоторое время. Продолжить?",
	updateConfirm: "Обновить"
};
var toasts$7 = {
	downloadStarted: "Загрузка началась. Проверьте UNIFIDECK > Загрузки",
	downloadQueued: "Загрузка в очереди",
	downloadQueuedBody: "Игра {{title}} добавлена в очередь.",
	downloadFailedUnknown: "Неизвестная ошибка.",
	updateQueued: "Обновление в очереди",
	uninstallDone: "Удаление завершено.",
	ubisoftLauncherOpening: "Запуск Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect запускается для {{title}}. Установите игру там, а затем вернитесь, чтобы играть.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect уже запущен для этой установки.",
	ubisoftLauncherOpenFailed: "Не удалось запустить Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Не удалось запустить Ubisoft Connect для установки.",
	multipartDetected: "Обнаружена многокомпонентная загрузка",
	multipartMessage: "Пожалуйста, подождите завершения процесса",
	downloadFailed: "Загрузка не удалась",
	downloadFailedMessage: "Не удалось начать загрузку.",
	downloadCancelled: "Загрузка отменена",
	downloadCancelledMessage: "Загрузка {{title}} отменена.",
	cancelFailed: "Не удалось отменить",
	cancelFailedMessage: "Не удалось отменить загрузку.",
	uninstalling: "Удаление...",
	uninstallingMessage: "Удаление {{title}}...",
	uninstallingMessageProton: "Удаление {{title}} и файлов Proton...",
	uninstallComplete: "Удаление завершено",
	uninstallCompleteMessage: "Игра {{title}} удалена.",
	uninstallCompleteMessageProton: "Игра {{title}} удалена (включая файлы Proton).",
	installComplete: "Установка завершена!",
	installCompleteMessage: "Игра {{title}} готова к запуску.",
	syncComplete: "Синхронизация завершена!",
	syncCompleteMessage: "Добавлено игр: {{count}}. ПЕРЕЗАПУСТИТЕ STEAM (выйдите полностью, а не просто вернитесь в режим игры), чтобы увидеть их в библиотеке.",
	forceSyncComplete: "Принудительная синхронизация завершена!",
	forceSyncCompleteMessage: "Обновлено игр: {{count}}. ПЕРЕЗАПУСТИТЕ STEAM для применения изменений.",
	syncCancelled: "Синхронизация отменена",
	authTimeout: "Время ожидания авторизации истекло",
	authTimeoutMessage: "Время авторизации в {{store}} истекло. Пожалуйста, попробуйте снова.",
	authFailed: "Авторизация не удалась",
	authFailedMessage: "Не удалось начать авторизацию",
	authError: "Ошибка авторизации",
	authConnected: "Магазин {{store}} подключен!",
	authConnectedMessage: "Успешная авторизация в {{store}}",
	cleanupStarted: "Очистка запущена",
	cleanupStartedMessage: "Удаление ярлыков, обложек и сохраненных учетных данных. Это может занять некоторое время…",
	cleanupSuccessful: "Очистка успешно завершена",
	cleanupSuccessfulMessage: "Удалено ярлыков: {{games}}, файлов обложек: {{artwork}}, выполнен выход из магазинов: {{stores}}, удалено папок установки: {{files}}. Кэш очищен.",
	cleanupNoop: "Нечего чистить",
	cleanupNoopMessage: "У Unifideck нет ярлыков, обложек или сохраненных учетных данных для удаления.",
	deleteFailed: "Не удалось удалить",
	repairPrefixSuccess: "Префикс исправлен",
	repairPrefixSuccessMessage: "Префикс Ubisoft Connect успешно пересобран",
	repairPrefixFailed: "Не удалось исправить",
	repairPrefixFailedMessage: "Не удалось исправить префикс: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Ошибка сети",
		launchRetry: "Повторный запуск",
		retryingUmu: "Повторная попытка запуска UMU через {{seconds}} сек. (попытка {{attempt}}/{{max}})...",
		protonUpgrade: "Обновление Proton",
		resettingPrefix: "Сброс префикса для {{version}}...",
		firstTimeSetup: "Первичная настройка",
		initializingPrefix: "Инициализация префикса Wine...",
		setupCompleteTitle: "Настройка завершена",
		prefixInitialized: "Префикс Wine инициализирован",
		setupFallback: "Настройка",
		fallbackInitialization: "Резервная инициализация...",
		launchingGame: "Запуск игры",
		startingEpicGame: "Запуск игры Epic...",
		startingAmazonGame: "Запуск игры Amazon...",
		startingGogGame: "Запуск игры GOG...",
		startingUbisoftGame: "Запуск игры Ubisoft...",
		dependenciesTitle: "Установка зависимостей",
		installingRedistributables: "Установка распространяемых компонентов игры...",
		dependenciesReady: "Зависимости готовы",
		redistributablesInstalled: "Компоненты игры установлены",
		dependenciesStatus: "Зависимости",
		checkLogs: "Настройка завершена (детали см. в журнале)",
		installingProton: "Установка Proton",
		attemptingInstall: "Попытка установить {{version}}...",
		epicAuthRequired: "Требуется авторизация Epic",
		epicAuthRequiredMessage: "Пожалуйста, войдите в Epic Games в настройках Unifideck",
		installingRedist: "Установка библиотек",
		installingRedistMessage: "Запуск первоначальной установки библиотек GOG...",
		setupFailed: "Настройка не удалась",
		setupFailedMessage: "Установка не удалась, подробности смотрите в логах",
		prerequisitesTitle: "Установка компонентов",
		installingPrerequisites: "Установка необходимых компонентов игры...",
		prerequisitesReady: "Компоненты готовы",
		prerequisitesInstalled: "Компоненты игры успешно установлены",
		prerequisitesFailed: "Предупреждение о компонентах",
		prerequisitesFailedMessage: "Некоторые компоненты игры могли быть установлены некорректно",
		browserRequired: "Требуется Microsoft Edge",
		signingIn: "Вход в систему...",
		cloudSyncDownFailed: "Не удалось скачать сохранения из облака для {{store}} ({{error}}). Игра запущена с локальными сохранениями.",
		cloudSyncUpFailed: "Не удалось загрузить сохранения в облако для {{store}} ({{error}}). Ваш прогресс сохранен только локально.",
		errorCircuitBreakerOpen: "Запуск {{game_key}} не удался {{count}} раз подряд. Автоматические запуски приостановлены.",
		launcherError: "Не удалось запустить {{game_key}} ({{error_code}}). Подробности смотрите в логах.",
		downloadingProton: "Загрузка Proton — это может занять несколько минут...",
		downloadingRuntime: "Загрузка Steam Runtime — первый запуск может занять несколько минут...",
		installingUbisoft: "Установка через Ubisoft Connect",
		installingUbisoftMessage: "Открытие Ubisoft Connect — начните установку в окне лаунчера.",
		protonReadyBody: "{{version}} готов к использованию",
		protonReadyTitle: "Proton готов",
		protonSwitchedTo: "Теперь используется {{version}}",
		signingInUbisoft: "Вход в Ubisoft Connect",
		signingInUbisoftMessage: "Открытие Ubisoft Connect — войдите там, затем вернитесь.",
		ubisoftPrefixNotReady: "Ubisoft Connect не готов",
		ubisoftPrefixNotReadyMessage: "Не удалось подготовить префикс Ubisoft Connect для этой игры. Попробуйте установить ещё раз; если ошибка повторяется, запустите синхронизацию библиотеки снова.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect не готов",
		ubisoftAuthPrefixNotReadyMessage: "Не удалось подготовить Ubisoft Connect для входа. Попробуйте ещё раз через мгновение.",
		ubisoftLaunchIdMissing: "Открытие Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "Не удалось определить ID запуска этой игры — открывается Ubisoft Connect, чтобы вы могли запустить её из своей библиотеки. Запустите синхронизацию библиотеки снова, чтобы исправить прямой запуск."
	},
	updateComplete: "Обновление завершено!",
	updateCompleteMessage: "{{title}} обновлен.",
	updateCancelled: "Обновление отменено",
	updateCancelledMessage: "Обновление {{title}} было отменено.",
	updateFailed: "Обновление не удалось",
	updateFailedMessage: "Не удалось начать обновление.",
	storeError: "Не удалось связаться с магазином {{store}} ({{errType}})",
	captureLogsStarted: "Сбор журналов...",
	captureLogsStartedMessage: "Создание диагностического архива в папке «Загрузки». Это займёт несколько секунд.",
	captureLogsDone: "Журналы собраны",
	captureLogsDoneMessage: "Сохранено в {{path}} ({{size}}, файлов: {{files}}).",
	captureLogsPartial: "Журналы собраны (частично)",
	captureLogsPartialMessage: "Сохранено в {{path}} ({{size}}, файлов: {{files}}). Не удалось прочитать файлов: {{failures}}.",
	captureLogsFailed: "Не удалось собрать журналы",
	captureLogsBusy: "Сбор журналов уже выполняется",
	captureLogsCopied: "Путь скопирован",
	captureLogsCopyFailed: "Не удалось скопировать путь",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Освободить место",
		signInToStore: "Войти",
		retrySync: "Повторить",
		retryLibrarySync: "Обновить библиотеку",
		openSaveFolder: "Открыть папку сохранений",
		showLogs: "Показать логи"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Не удалось обновить библиотеку магазина {{store}} ({{error}}). Текущий список игр останется без изменений."
	},
	cloudPullStarted: "Скачивание облачного сохранения…",
	cloudPushStarted: "Загрузка локального сохранения…",
	cloudSavePathFailed: "Не удалось установить папку сохранений",
	cloudSavePathSet: "Папка сохранений обновлена",
	cloudPullDone: "Облачное сохранение скачано",
	cloudPullFailed: "Не удалось скачать облачное сохранение",
	cloudPushDone: "Локальное сохранение загружено",
	cloudPushFailed: "Не удалось загрузить локальное сохранение"
};
var uninstallModal$7 = {
	title: "Подтверждение удаления",
	description: "Вы уверены, что хотите удалить {{title}}?",
	deleteProtonLabel: "Удалить также файлы Proton",
	deleteProtonDescription: "Удаляет данные совместимости и пользовательские файлы",
	warningTitle: "Предупреждение:",
	warningBody: "Это навсегда удалит сохранения игры, хранящиеся внутри префикса Wine. Убедитесь, что у вас есть резервная копия.",
	cancel: "Отмена",
	uninstall: "Удалить"
};
var languageSettings$7 = {
	title: "ЯЗЫК",
	label: "Язык интерфейса",
	description: "Язык интерфейса и загружаемых игр (если поддерживается)",
	autoDetect: "Авто (Системный)"
};
var errors$7 = {
	legendaryNotFound: "Утилита Epic Games CLI (Legendary) не найдена",
	gogdlNotFound: "Утилита GOG CLI (gogdl) не найдена",
	nileNotFound: "Утилита Amazon Games CLI (Nile) не найдена",
	authUrlFailed: "Не удалось получить ссылку для авторизации",
	authFailed: "Авторизация не удалась",
	notAuthenticated: "Не авторизовано. Пожалуйста, сначала войдите в систему.",
	syncInProgress: "Синхронизация уже выполняется",
	syncCancelled: "Синхронизация отменена пользователем",
	gameNotFound: "Игра не найдена",
	gameNotInstalled: "Игра не установлена",
	installFailed: "Установка завершилась ошибкой — подробности см. в логах",
	installNoDirectory: "Установка завершена, но не удалось найти папку игры",
	uninstallFailed: "Не удалось удалить игру",
	lockConflict: "Конфликт блокировки — пожалуйста, попробуйте позже",
	unknownStore: "Неизвестный магазин",
	shortcutWriteFailed: "Не удалось записать ярлыки",
	backgroundSyncDisabled: "Фоновая синхронизация отключена",
	steamGridDbUnavailable: "Библиотека SteamGridDB недоступна",
	deleteSyncInProgress: "Нельзя проводить удаление во время синхронизации",
	alreadyInQueue: "Уже в очереди",
	download: {
		diskSpace: "Недостаточно свободного места на диске",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Ошибка сети — проверьте подключение к Интернету",
		authExpired: "Сессия авторизации истекла — пожалуйста, войдите заново",
		toolMissing: "Требуемая утилита скачивания не найдена",
		processFailed: "Загрузка прервалась — пожалуйста, попробуйте еще раз",
		directoryNotFound: "Не удалось найти установленные файлы игры",
		lockConflict: "Выполняется другая операция — попробуйте позже",
		ubisoftClient: "Ошибка клиента Ubisoft Connect — попробуйте исправить префикс",
		ubisoftClientNotRunning: "Клиент Ubisoft Connect не отвечает",
		ubisoftBootstrap: "Не удалось настроить префикс Ubisoft Connect",
		ubisoftUmuMissing: "Не найден инструмент umu-run — требуется для игр Ubisoft",
		generic: "Установка завершилась ошибкой — подробности см. в логах"
	},
	unknown: "Неизвестная ошибка",
	steamAppsUnavailable: "API SteamClient.Apps недоступно"
};
var sync$7 = {
	start: "Синхронизировать библиотеки",
	force: "Принудительная синхронизация",
	syncing: "Синхронизация…",
	cancel: "Отменить синхронизацию",
	cancelling: "Отмена…",
	preparing: "Подготовка…",
	downloadingArtwork: "Скачивание обложки для {{game}}...",
	fetchingGameLists: "Получение списка игр...",
	fetchingStore: "Получение игр {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Синхронизация отменена пользователем",
	checkingInstalledGames: "Проверка установленных игр...",
	sgdbLookup: "Поиск {{count}} игр на SteamGridDB...",
	extractingSteamMetadata: "Извлечение метаданных Steam...",
	fetchingEnhancedMetadata: "Получение расширенных метаданных...",
	lookingUpUnifiDB: "Получение метаданных unifiDB...",
	fetchingMetacriticData: "Получение метаданных Metacritic...",
	fetchingCompatData: "Проверка совместимости...",
	checkingExistingArtwork: "Проверка существующих обложек...",
	cleaningOrphanedArtwork: "Очистка неиспользуемых обложек...",
	retryingMissingArtwork: "Повторная попытка для {{synced}} / {{total}} игр с отсутствующими обложками...",
	savingShortcuts: "Сохранение ярлыков...",
	completed: "Синхронизация завершена!",
	extractingMetadata: "Извлечение метаданных для {{game}}...",
	cooldown: "Подождите {{secs}} сек.",
	artworkPhase: "Обложка {{done}} / {{total}}"
};
var force_sync$7 = {
	migratingOldInstallations: "Принудительная синхронизация: Перенос старых установок...",
	fetchingGameLists: "Принудительная синхронизация: Получение списков игр...",
	checkingInstalledGames: "Принудительная синхронизация: Проверка установленных игр...",
	rewritingShortcuts: "Принудительная синхронизация: Перезапись ярлыков...",
	cancelledShortcutsSaved: "Принудительная синхронизация отменена — ярлыки сохранены",
	writingShortcuts: "Принудительная синхронизация: Запись ярлыков...",
	clearingProton: "Принудительная синхронизация: Очистка префиксов совместимости Proton...",
	completed: "Принудительная синхронизация завершена! Обновлено игр: {{updated}}, скачано обложек: {{artwork}}."
};
var artwork$7 = {
	checking: "Проверка обложек...",
	queueRefresh: "Добавление всех игр в очередь на обновление обложек...",
	downloadProgress: "Скачано обложек: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$7 = {
	greatOnDeck: "Отлично на Deck",
	allGames: "Все игры",
	installed: "Установлено",
	nonSteam: "Сторонние игры",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$7 = {
	error: "Ошибка Unifideck",
	errorLoadingView: "Не удалось загрузить единую библиотеку игр",
	checkConsole: "Подробности смотрите в консоли браузера",
	errorLoadingGames: "Ошибка при загрузке списка игр",
	tryReloading: "Попробуйте обновить плагин или посмотрите консоль",
	storeLabel: "Магазин:",
	allStores: "Все магазины",
	searchPlaceholder: "Поиск игр...",
	library: "Библиотека"
};
var gameInfoPanel$7 = {
	loading: "Загрузка информации об игре...",
	noMetadata: "Нет доступных метаданных для этой игры.",
	noTestResults: "Нет детальных результатов тестирования.",
	buttons: {
		details: "Подробности",
		synopsis: "Описание",
		storePage: "Страница магазина",
		dlc: "DLC",
		communityHub: "Центр сообщества",
		pointsShop: "Магазин очков",
		discussions: "Обсуждения",
		guides: "Руководства",
		support: "Поддержка",
		install: "Установить",
		uninstall: "Удалить",
		cancel: "Отмена",
		achievements: "Достижения"
	},
	labels: {
		store: "Магазин:",
		name: "Название:",
		size: "Размер:",
		developer: "Разработчик:",
		publisher: "Издатель:",
		released: "Выпущено:",
		metacritic: "Metacritic:",
		status: "Статус:",
		spaceRequired: "Требуется места",
		lastPlayed: "Последний запуск",
		playTime: "Время в игре",
		never: "Никогда"
	},
	compatibility: {
		modalTitle: "Совместимость со Steam Deck",
		viewOnProtonDb: "Посмотреть на ProtonDB",
		close: "Закрыть",
		verified: "СОВМЕСТИМО",
		playable: "МОЖНО ИГРАТЬ",
		unsupported: "НЕ ПОДДЕРЖИВАЕТСЯ",
		unknown: "НЕИЗВЕСТНО"
	},
	failedToLoadMetadata: "Не удалось загрузить метаданные",
	lastSession: "Прошлая сессия: получено {{names}}"
};
var gameDetailsSettings$7 = {
	title: "ВИД СТРАНИЦЫ ИГРЫ",
	simple: "Простой",
	detailed: "Подробный"
};
var gogLanguageModal$7 = {
	title: "Выбрать язык",
	description: "Выберите язык загрузки для игры {{title}}",
	label: "Язык",
	install: "Установить",
	cancel: "Отмена"
};
var accountSwitch$7 = {
	title: "Обнаружена смена аккаунта",
	description: "Вы вошли под другим аккаунтом Steam. Что вы хотите сделать с данными Unifideck?",
	migrate: "Перенести данные Unifideck",
	migrateDescription: "Копировать игры и обложки из предыдущего аккаунта",
	freshStart: "Начать заново",
	freshStartDescription: "Очистить авторизацию магазинов для подключения других аккаунтов Epic/GOG/Amazon/Ubisoft",
	skip: "Пропустить",
	toastMigrateTitle: "Перенос завершен",
	toastMigrateBody: "Перенесено ярлыков: {{shortcuts}}, обложек: {{artwork}}",
	toastClearTitle: "Токены авторизации удалены",
	toastClearBody: "Авторизация в магазинах была сброшена. Теперь вы можете войти под другими аккаунтами."
};
var ubisoftAuth$7 = {
	title: "Вход в Ubisoft Connect",
	description: "Войдите в Ubisoft Connect, чтобы привязать вашу библиотеку.",
	signInViaUpc: "Войти через Ubisoft Connect",
	upcDescription: "Ubisoft Connect откроется для входа. После успешного входа сессия будет автоматически импортирована.",
	launchingUpc: "Запуск Ubisoft Connect...",
	waitingForLogin: "Ожидание входа...",
	retryLaunch: "Повторить попытку",
	cancel: "Отмена",
	launchFailed: "Не удалось запустить Ubisoft Connect. Пожалуйста, попробуйте еще раз.",
	signingIn: "Вход...",
	emailLabel: "Электронная почта",
	passwordLabel: "Пароль",
	signIn: "Войти",
	twoFactorTitle: "Двухфакторная аутентификация",
	twoFactorDescription: "Введите код проверки из вашего приложения-аутентификатора.",
	twoFactorLabel: "Код проверки",
	verify: "Проверить",
	verifying: "Проверка...",
	errorEmptyFields: "Пожалуйста, введите почту и пароль",
	errorEmpty2FA: "Пожалуйста, введите код проверки",
	errorLoginFailed: "Не удалось войти. Пожалуйста, проверьте учетные данные.",
	errorInvalid2FA: "Неверный код проверки. Пожалуйста, попробуйте еще раз.",
	showPassword: "Показать пароль",
	hidePassword: "Скрыть пароль",
	doubleLoginWarning: "Вам потребуется войти в Ubisoft дважды."
};
var relativeDate$7 = {
	today: "Сегодня",
	yesterday: "Вчера"
};
var common$7 = {
	thisGame: "эту игру",
	game: "Игра",
	unknown: "Неизвестно",
	ok: "ОК",
	cancel: "Отмена",
	working: "Обработка…",
	loading: "Загрузка…",
	free: "бесплатно",
	close: "Закрыть"
};
var info$7 = {
	expandDetails: "Развернуть",
	collapseDetails: "Свернуть"
};
var microsoft$7 = {
	notCompatible: "Этот формат игры несовместим со SteamOS",
	playOnCloud: "Играть в облаке",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Войдите со своей учетной записью Microsoft / Xbox",
	accountConnected: "Учетная запись Microsoft подключена",
	loggedOut: "Выход из Microsoft Store выполнен",
	xcloudNotInstalled: "Игры xCloud не устанавливаются локально",
	chromiumRequired: "Требуется Microsoft Edge",
	chromiumRequiredMessage: "Для входа требуется Microsoft Edge. Установить его автоматически? (Это требуется сделать только один раз).",
	chromiumInstallButton: "Установить Microsoft Edge",
	chromiumInstalling: "Установка Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge установлен!",
	chromiumAlreadyInstalled: "Совместимый браузер уже установлен",
	chromiumInstallFailed: "Не удалось установить Microsoft Edge. Пожалуйста, установите вручную: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Превышено время ожидания установки Microsoft Edge. Проверьте подключение к Интернету.",
	browserAlreadyInstalled: "Совместимый браузер уже установлен",
	browserInstalled: "Microsoft Edge успешно установлен!",
	browserInstallFailed: "Не удалось установить Microsoft Edge. Пожалуйста, установите вручную: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak недоступен в этой системе",
	signInRequired: "Войдите, чтобы играть",
	syncSkippedNoSubscription: "Активная подписка Xbox Game Pass не найдена. Ваша библиотека xCloud не синхронизирована.",
	syncSkippedTierUnknown: "Подписка Xbox Game Pass обнаружена, но уровень определить не удалось. Синхронизация пропущена во избежание показа недоступных игр.",
	subscriptionCheckFailed: "Не удалось проверить подписку Xbox Game Pass. Синхронизация xCloud пропущена. Попробуйте еще раз, когда подключение станет стабильным.",
	subscriptionDetected: "Обнаружена активная подписка Xbox Game Pass {{tier}}."
};
var playButton$7 = {
	play: "Играть",
	install: "Установить",
	cancel: "Отмена",
	retry: "Повторить",
	downloadFailed: "Загрузка не удалась",
	installing: "Установка",
	controllerConfig: "Настройки контроллера",
	appSettings: "Настройки приложения"
};
var play$7 = {
	play: "Играть",
	resume: "Продолжить",
	stop: "Остановить",
	cancel: "Отмена",
	cancelling: "Отмена…",
	update: "Обновить",
	uninstall: "Удалить",
	queued: "В очереди",
	downloading: "Загрузка",
	extracting: "Распаковка",
	verifying: "Проверка",
	finalizing: "Завершение установки",
	cancelConfirmTitle: "Отменить загрузку?",
	cancelConfirmBody: "Вы уверены, что хотите отменить загрузку {{title}}? Частично загруженные файлы будут удалены.",
	cancelConfirmConfirm: "Отменить загрузку",
	cancelConfirmCancel: "Продолжить загрузку",
	playOnCloud: "Играть в облаке",
	cloudSave: {
		autoPullOn: "Облачные сохранения загружаются автоматически при запуске.",
		close: "Закрыть",
		download: "Скачать облачное сохранение",
		filesCount: "{{count}} файлов",
		label: "Облачные сохранения",
		menuTitle: "Облачные сохранения — {{game}}",
		noCloudData: "Неизвестно",
		noCloudSupport: "Нет поддержки облачных сохранений",
		setLocation: "Указать папку сохранений",
		setLocationHint: "Выберите папку, в которой эта игра хранит свои сохранения.",
		setLocationTitle: "Выберите папку сохранений этой игры",
		statusCloudAvailable: "Облачное сохранение доступно",
		statusNoSaves: "Нет локальных сохранений",
		statusSyncing: "Синхронизация облачных сохранений…",
		statusUnresolved: "Папка сохранений не найдена — синхронизация может не работать для этой игры.",
		unresolvedHint: "Запустите игру один раз, чтобы мы могли определить ее папку сохранений, или укажите ее вручную.",
		upload: "Загрузить локальное сохранение"
	},
	exe: {
		browse: "Обзор…",
		changeFailed: "Не удалось изменить исполняемый файл",
		changed: "Исполняемый файл изменен",
		defaultTag: "(по умолчанию)",
		menuItem: "Изменить исполняемый файл…",
		reset: "Сбросить на исполняемый файл по умолчанию",
		resetButton: "Сбросить по умолчанию",
		subtitle: "Выберите, какой файл запускает эту игру (например, чтобы пропустить лаунчер и запустить игру напрямую). Рабочая папка и облачные сохранения не изменятся.",
		title: "Изменить исполняемый файл — {{game}}",
		unavailable: "Не удалось прочитать папку этой игры."
	}
};
var playMeta$7 = {
	spaceRequired: "Требуется места",
	installedSize: "Размер на диске",
	lastPlayed: "Последний запуск",
	neverPlayed: "Ни разу не запускалась",
	hoursShort: "ч.",
	minutesShort: "мин.",
	played: "Сыграно"
};
var downloads$7 = {
	title: "Загрузки",
	empty: "Загрузок нет. Установите игры из своей библиотеки, чтобы увидеть их здесь.",
	current: "Загружается сейчас",
	queued: "В очереди",
	finished: "Недавно завершенные",
	cancel: "Отмена",
	removeFromQueue: "Удалить",
	play: "Играть",
	outcome: {
		queued: "В очереди",
		downloading: "Загрузка",
		completed: "Завершено",
		cancelled: "Отменено",
		error: "Ошибка"
	}
};
var navigation$7 = {
	loading: "Загрузка...",
	storesTitle: "Магазины",
	syncTitle: "Синхр. библиотеки",
	languageTitle: "Язык"
};
var security$7 = {
	panelTitle: "Безопасность",
	counters: {
		label: "Счетчики",
		empty: "Событий с момента запуска не обнаружено"
	},
	log: {
		label: "Журнал аудита",
		entryCount: "{{count}} запись",
		entryCount_other: "{{count}} записей",
		emptyFiltered: "Событий, соответствующих фильтру, не обнаружено"
	},
	filter: {
		label: "Фильтр",
		all: "Все события"
	},
	timeline: {
		label: "Хроника за 24 ч ({{count}} событие)",
		label_other: "Хроника за 24 ч ({{count}} событий)",
		axisStart: "-24 ч",
		axisEnd: "сейчас"
	},
	relativeTime: {
		seconds: "{{count}} сек. назад",
		minutes: "{{count}} мин. назад",
		hours: "{{count}} ч. назад",
		days: "{{count}} дн. назад"
	},
	bruteforce: {
		warningTitle: "Предупреждение: обнаружен перебор паролей (уровень предупреждения)",
		escalationTitle: "АЛАРМ: обнаружен перебор паролей (уровень эскалации)",
		detail: "{{failures}} неудачных попыток расшифровки за последние {{window}} сек. Пороги: {{warning}} (предупреждение) / {{escalation}} (эскалация).",
		resetButton: "Подтвердить и сбросить детектор"
	},
	actions: {
		clearLog: "Очистить журнал и счетчики"
	},
	events: {
		tokenEncrypted: "Токен зашифрован",
		tokenDecrypted: "Токен расшифрован",
		decryptFailed: "Не удалось расшифровать",
		tokenFileMigrated: "Файл токенов перенесен",
		legacyPlaintextDetected: "Обнаружен устаревший открытый текст",
		authFlowStarted: "Авторизация запущена",
		authFlowCompleted: "Авторизация завершена",
		authFlowFailed: "Авторизация не удалась",
		permissionsCheck: "Права доступа проверены",
		permissionsRepaired: "Права доступа восстановлены",
		bruteforceSuspected: "Подозрение на перебор паролей",
		deviceResetDetected: "Обнаружен сброс устройства",
		fingerprintInitialized: "Отпечаток инициализирован",
		externalAuthCheckFailed: "Проверка внешней авторизации завершилась ошибкой",
		configValidationCompleted: "Проверка конфигурации завершена",
		configValidationFailed: "Проверка конфигурации не удалась"
	},
	configValidation: {
		degradedTitle: "Неверная конфигурация: активен режим ограниченной функциональности",
		detail: "Обнаружена {{count}} ошибка конфигурации при запуске",
		detail_other: "Обнаружено {{count}} ошибок конфигурации при запуске",
		sourceDefaults: "значения по умолчанию",
		sourceUser: "настройки пользователя",
		sourceMerged: "объединенные",
		overflow: "+ {{count}} другая ошибка",
		overflow_other: "+ {{count}} других ошибок"
	}
};
var cloudSyncBehavior$7 = {
	_comment: "Панель настроек, позволяющая пользователю выбрать для каждого магазина, как показывать ошибки синхронизации сохранений. Пять записей store_label соответствуют строкам, отображаемым CloudSyncBehaviorPanel.tsx. Microsoft намеренно опущена: облачные игры xCloud не имеют локальных сохранений.",
	title: "Синхронизация сохранений",
	description: "Выберите поведение Unifideck при сбое загрузки или отправки облачных сохранений во время запуска игры.",
	loadError: "Не удалось загрузить текущие настройки",
	saveError: "Не удалось сохранить изменения. Пожалуйста, попробуйте еще раз",
	store_label: {
		"default": "По умолчанию (все магазины)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Тихий режим (только в журнал)",
		toast: "Уведомлять (всплывающее сообщение)"
	}
};
var cloudSync$7 = {
	_comment: "Всплывающие сообщения, отправляемые лаунчером при сбое облачной синхронизации. Описание ошибки сопоставляется через cloudSync.error.<code> с использованием поля error_i18n_key из ответа бэкенда.",
	error: {
		network_unreachable: "Сеть недоступна",
		auth_expired: "Срок сессии авторизации истек",
		forbidden: "Доступ запрещен",
		quota_exceeded: "Превышена квота облачного хранилища",
		server_error: "Сервер магазина недоступен",
		timed_out: "Время ожидания запроса истекло",
		disk_full: "Недостаточно места на диске",
		disk_space_low: "Пропущено: мало места на диске (освободите место и попробуйте снова)",
		permission_denied: "Доступ ограничен",
		cancelled: "Отменено",
		unknown: "Непредвиденная ошибка"
	},
	unavailableNativeDep: "Облачные сохранения отключены на этой системе — в её Python ({{python}}) отсутствует необходимый компонент. Это не повлияет на локальные сохранения; пересоберите или переустановите плагин после обновления системы, чтобы восстановить синхронизацию."
};
var launchLogs$7 = {
	_comment: "Сообщения для LaunchLogsModal. Модальное окно открывается при переходе по ссылке unifideck://show-logs/<launch_id>, прикрепленной к уведомлениям о сбое лаунчера (circuit breaker + LauncherError). Читает архивные логи запуска из ~/.local/share/unifideck/launches/<launch_id>.log и выводит их с цветовой разметкой по важности.",
	title: "Логи запуска — {{launchId}}",
	close: "Закрыть",
	copyAll: "Копировать все",
	"export": "Экспорт в файл",
	copied: "Скопировано в буфер обмена",
	copyFailed: "Не удалось скопировать",
	exporting: "Экспорт…",
	exportedTo: "Экспортировано в {{path}}",
	exportFailed: "Ошибка экспорта: {{error}}",
	loading: "Загрузка логов…",
	notAvailable: "Логи этого запуска больше не доступны (истек 7-дневный срок хранения).",
	error: "Не удалось прочитать логи: {{error}}",
	fileInfo: "{{path}} — показано {{shown}} из {{total}} строк",
	filterLabel: "Показать",
	filterInfo: "Инфо",
	filterWarning: "Предупреждение",
	filterError: "Ошибка",
	nextError: "Следующая ошибка ({{current}}/{{total}})",
	noErrors: "Ошибок нет",
	allFiltered: "Все строки скрыты текущим фильтром. Включите хотя бы один уровень важности, чтобы увидеть записи."
};
var settings$7 = {
	_comment: "Элементы управления UnifideckSettingsPanel, не вошедшие в другие компоненты (LanguageSelector, CloudSyncBehaviorPanel). refreshAll — кнопка, запускающая sync_all() для всех настроенных магазинов; библиотека выводит прогресс синхронизации через подписку на события SYNC_PROGRESS.",
	storeConnections: "Подключение магазинов",
	librarySync: "Синхр. библиотеки",
	language: "Язык",
	languageAuto: "Определять автоматически",
	refreshAll: {
		sectionTitle: "Действия с библиотекой",
		button: "Обновить все библиотеки",
		buttonBusy: "Запуск…",
		startedTitle: "Обновление библиотеки запущено",
		startedBody: "Синхронизация всех магазинов выполняется в фоновом режиме.",
		startedSingle: "Обновление библиотеки {{store}} выполняется в фоновом режиме.",
		failedTitle: "Не удалось запустить обновление библиотеки"
	}
};
var saveFolder$7 = {
	_comment: "Сообщения для SaveFolderModal. Модальное окно открывается при переходе по ссылке unifideck://open-save-folder/<store>/<game_id>, как правило, при сбое облачной синхронизации, когда пользователь хочет просмотреть локальные файлы. count и size подставляются; truncated выводится только при ограничении вывода списка.",
	title: "Папка сохранений — {{store}} / {{gameId}}",
	close: "Закрыть",
	copyPath: "Копировать путь",
	copyFileList: "Копировать список файлов",
	depthLabel: "Глубина",
	filterLabel: "Фильтр",
	loading: "Загрузка содержимого папки…",
	noSavesYet: "Локальных сохранений для этой игры пока не найдено. Папка будет создана при следующей успешной облачной синхронизации.",
	aggregate: "Файлов: {{count}}, общий объем: {{size}}",
	truncated: "Еще {{count}} файлов ({{size}}) не показано. Уточните фильтр для просмотра.",
	error: "Не удалось прочитать папку сохранений: {{error}}"
};
var library$7 = {
	_comment: "Поиграновые службы интерфейса в библиотеке, включая индикатор автоматического выключателя запусков и элементы принудительного запуска / сброса ошибок при повторных сбоях.",
	circuitBreaker: {
		_comment: "Интерфейс для предохранителя сбоев запуска. Индикатор появляется, только если число ошибок в заданном интервале достигает circuit_breaker.failures_threshold (по умолчанию 3 за 600 сек). Принудительный запуск выполняет однократный обход защиты; Сброс очищает историю сбоев.",
		badgeRecent: "{{count}} недавний сбой запуска",
		badgeRecent_other: "{{count}} недавних сбоев запуска",
		badgeTooltip: "{{count}} неудачных запусков за последнее время. Предохранитель приостановил автоматические запуски.",
		forceLaunchButton: "Запустить принудительно",
		resetButton: "Сбросить ошибки",
		failureKind: {
			fastBoot: "Вылет при быстром запуске",
			launcherError: "Ошибка подготовки лаунчера"
		}
	}
};
var pickStorage$7 = {
	title: "Папка установки",
	customOption: "Выбрать другую папку...",
	customLabel: "Пользовательская папка",
	change: "Изменить...",
	gameSize: "Размер установки"
};
var restart$7 = {
	title: "Синхронизация завершена!",
	body: "Пожалуйста, перезапустите Steam для применения ваших новых игр и обложек",
	confirm: "Перезапустить сейчас",
	later: "Позже"
};
var updater$7 = {
	checkButton: "Проверить обновления",
	checkCompleteMessage: "Информация о последней версии успешно получена.",
	checkCompleteTitle: "Проверка обновлений завершена",
	checkFailedTitle: "Ошибка проверки",
	checkingButton: "Проверка...",
	currentTitle: "Текущая",
	downgradeButton: "Откат к версии v{{version}}",
	installFailedTitle: "Установка не удалась",
	installTimeoutMessage: "Нет ответа от Decky Loader — возможно, установка зависла или завершилась ошибкой. Пожалуйста, попробуйте снова.",
	installedLabel: "установлено",
	installingButton: "Установка...",
	installingTitle: "Установка плагина",
	latestLabel: "последняя",
	modalTitle: "UNIFIDECK v{{version}} — Описание выпуска",
	noBackend: "Бэкенд Decky недоступен.",
	noReleaseNotes: "Описание изменений отсутствует.",
	reinstallButton: "Переустановить версию v{{version}}",
	refreshFailedMessage: "Не удалось проверить информацию о последней версии. Проверьте подключение и попробуйте снова.",
	releaseGoneMessage: "Этот выпуск больше недоступен. Проверьте обновления и выберите снова.",
	releaseNotesButton: "Описание изменений",
	stageDownload: "Загрузка…",
	stageFinish: "Завершение…",
	stageInstall: "Установка…",
	stageOpen: "Чтение пакета…",
	stageParse: "Проверка…",
	stageRemove: "Удаление старой версии…",
	stageStart: "Запуск…",
	stageWorking: "Установка…",
	titleLoading: "Проверка версии",
	typeDowngrade: "Откат к версии",
	typeReinstall: "Переустановка версии",
	typeUpdate: "Обновление до версии",
	updateButton: "Обновить до v{{version}}"
};
var achievements$7 = {
	unlockedTitle: "Достижение получено",
	unlockedBody: "{{name}}"
};
var achievementsModal$7 = {
	title: "Достижения",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Обновить",
	close: "Закрыть",
	loading: "Загрузка достижений…",
	empty: "В этой игре нет достижений.",
	hidden: "Скрытое достижение",
	unlockedOn: "Получено {{date}}",
	error: "Не удалось загрузить достижения.",
	errorOffline: "Подключитесь к интернету, чтобы просмотреть достижения.",
	errorAuth: "Войдите в магазин этой игры, чтобы просмотреть достижения.",
	errorUnavailable: "Достижения недоступны для этой игры."
};
var collectionSettings$7 = {
	title: "КОЛЛЕКЦИИ STEAM",
	enabled: "Коллекции включены",
	disabled: "Коллекции выключены"
};
var ruRU = {
	_comment: _comment$7,
	tabs: tabs$7,
	auth: auth$7,
	storage: storage$7,
	storageSettings: storageSettings$7,
	gameGrid: gameGrid$7,
	downloadsTab: downloadsTab$7,
	cloudSave: cloudSave$7,
	storeConnections: storeConnections$7,
	librarySync: librarySync$7,
	cleanup: cleanup$7,
	captureLogs: captureLogs$7,
	installButton: installButton$7,
	confirmModals: confirmModals$7,
	toasts: toasts$7,
	uninstallModal: uninstallModal$7,
	languageSettings: languageSettings$7,
	errors: errors$7,
	sync: sync$7,
	force_sync: force_sync$7,
	artwork: artwork$7,
	deckTabs: deckTabs$7,
	unifiedLibrary: unifiedLibrary$7,
	gameInfoPanel: gameInfoPanel$7,
	gameDetailsSettings: gameDetailsSettings$7,
	gogLanguageModal: gogLanguageModal$7,
	accountSwitch: accountSwitch$7,
	ubisoftAuth: ubisoftAuth$7,
	relativeDate: relativeDate$7,
	common: common$7,
	info: info$7,
	microsoft: microsoft$7,
	playButton: playButton$7,
	play: play$7,
	playMeta: playMeta$7,
	downloads: downloads$7,
	navigation: navigation$7,
	security: security$7,
	cloudSyncBehavior: cloudSyncBehavior$7,
	cloudSync: cloudSync$7,
	launchLogs: launchLogs$7,
	settings: settings$7,
	saveFolder: saveFolder$7,
	library: library$7,
	pickStorage: pickStorage$7,
	restart: restart$7,
	updater: updater$7,
	achievements: achievements$7,
	achievementsModal: achievementsModal$7,
	collectionSettings: collectionSettings$7
};

var _comment$6 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$6 = {
	settings: "Налаштування",
	downloads: "Завантаження"
};
var auth$6 = {
	connect: "Увійти",
	disconnect: "Вийти",
	status: {
		connected: "Підключено",
		disconnected: "Не підключено",
		checking: "Перевірка…",
		error: "Помилка — перевірте логи"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$6 = {
	title: "Сховище",
	defaultUpdated: "Типове сховище оновлено",
	isDefault: "За замовчуванням",
	setDefault: "Зробити типовим",
	customPath: "Власний шлях",
	up: "↑ Нагору",
	refresh: "Оновити",
	useThisPath: "Використовувати цей шлях"
};
var storageSettings$6 = {
	title: "Налаштування завантаження",
	installLocationLabel: "Папка встановлення",
	installLocationDescription: "Куди будуть завантажуватись нові ігри",
	pathLabel: "Шлях",
	loading: "Завантаження параметрів сховища...",
	toastUpdatedTitle: "Папку встановлення оновлено",
	toastUpdatedBody: "Нові ігри будуть встановлюватися в {{location}}",
	toastFailedTitle: "Не вдалося оновити",
	toastFailedBody: "{{error}}",
	unknown: "Невідомо",
	browseButton: "Огляд...",
	clearCustom: "Скинути шлях",
	customPathSet: "Встановлено користувацьку папку встановлення",
	newFolder: "Нова папка",
	newFolderCreate: "Створити",
	selectFolder: "Вибрати цю папку",
	emptyDirectory: "Порожня папка",
	showHidden: "Показувати приховані",
	selectDevice: "Вибрати накопичувач",
	sortAZ: "А-Я",
	sortZA: "Я-А",
	sortModifiedNewest: "Змінено (спочатку нові)",
	sortModifiedOldest: "Змінено (спочатку старі)",
	sortCreatedNewest: "Створено (спочатку нові)",
	sortCreatedOldest: "Створено (спочатку старі)",
	sortSizeLargest: "Розмір (за спаданням)",
	sortSizeSmallest: "Розмір (за зростанням)",
	"validationErrors.notAbsolute": "Шлях має бути абсолютним",
	"validationErrors.systemDir": "Не можна використовувати системну папку",
	"validationErrors.notWritable": "У цій папці немає прав на запис",
	"validationErrors.notFound": "Папка не існує",
	"validationErrors.homeRoot": "Не можна використовувати домашній кореневий каталог",
	freeSpaceLabel: "{{label}} ({{freeSpace}} ГБ вільно)"
};
var gameGrid$6 = {
	loading: "Завантаження ігор...",
	noGamesFound: "Ігор не знайдено",
	noGamesHint: "Спробуйте синхронізувати бібліотеки в налаштуваннях Unifideck",
	showingGames: "Показано ігор: {{count}}",
	installed: "Встановлено",
	notInstalled: "Не встановлено",
	playtime: "Зіграно: {{hours}} год. {{minutes}} хв."
};
var downloadsTab$6 = {
	currentDownload: "ПОТОЧНЕ ЗАВАНТАЖЕННЯ",
	queuedDownloads: "У ЧЕРЗІ ({{count}})",
	recentlyCompleted: "НЕЩОДАВНО ЗАВЕРШЕНІ",
	noActiveDownloads: "Немає активних завантажень",
	noDownloads: "Завантажень немає. Встановіть ігри зі своєї бібліотеки, щоб побачити їх тут.",
	loadingLabel: "Завантаження...",
	loadingMessage: "Отримання черги завантажень...",
	confirmCancelTitle: "Підтвердження скасування",
	confirmCancelDescription: "Ви впевнені, що хочете скасувати завантаження {{game}}?",
	confirmCancelYes: "Так",
	confirmCancelNo: "Ні",
	cancelDownload: "Скасувати",
	toastDownloadCancelledTitle: "Завантаження скасовано",
	toastDownloadCancelledBody: "Завантаження було видалено з черги.",
	toastCancelFailedTitle: "Не вдалося скасувати",
	toastCancelFailedBody: "Не вдалося скасувати завантаження: {{error}}",
	status: {
		queued: "У черзі",
		downloading: "Завантаження",
		completed: "Завершено",
		cancelled: "Скасовано",
		error: "Помилка"
	},
	preparingDownload: "Підготовка до завантаження...",
	phaseExtracting: "Розпакування {{game}}...",
	phaseVerifying: "Перевірка {{game}}...",
	internalStorage: "Внутрішня пам'ять ({{freeSpace}} ГБ вільно)",
	sdCard: "Карта SD ({{freeSpace}} ГБ вільно)",
	customLocation: "Інша папка: {{path}} ({{freeSpace}} ГБ вільно)",
	extractingLabel: "Розпакування...",
	verifyingLabel: "Перевірка...",
	updateQueuedLabel: "Оновлення в черзі",
	downloadQueuedLabel: "Завантаження в черзі",
	downloadingUpdateLabel: "Завантаження оновлення",
	downloadingLabel: "Завантаження...",
	finalizingInstallation: "Завершення встановлення...",
	speedMbps: "{{speed}} МБ/с",
	etaLabel: "Залишилося часу: {{eta}}",
	preparingMessage: "Ініціалізація префікса та синхронізація хмарних збережень...",
	preparingLabel: "Налаштування гри...",
	installingViaUpcLabel: "Встановлення через Ubisoft Connect",
	extractingMessage: "Розпакування файлів...",
	verifyingMessage: "Перевірка файлів... {{pct}}%",
	installingViaUpcMessage: "Дотримуйтесь вікна Ubisoft Connect, щоб завершити встановлення..."
};
var cloudSave$6 = {
	title: "Конфлікт хмарних збережень",
	description: "Локальні збереження для {{game}} відрізняються від хмарних",
	local: "Локальне:",
	cloud: "Хмарне:",
	newer: "(новіше)",
	useCloud: "Використовувати хмарне",
	useLocal: "Використовувати локальне",
	unknownDate: "Невідомо",
	toastUsingCloudTitle: "Використання хмарних збережень",
	toastUsingCloudBody: "Завантаження хмарних збережень для {{game}}",
	toastUsingLocalTitle: "Використання локальних збережень",
	toastUsingLocalBody: "Завантаження локальних збережень у хмару для {{game}}",
	toastErrorTitle: "Помилка",
	toastErrorBody: "Не вдалося вирішити конфлікт збережень",
	filesSize: "{{count}} файлів · {{size}}",
	uploadBlockedEmpty: "Завантаження в хмару для {{game}} пропущено — локальні збереження не знайдено",
	uploadSkippedTitle: "Завантаження в хмару пропущено",
	uploadSkippedBody: "Поки немає локальних збережень для завантаження для {{game}}."
};
var storeConnections$6 = {
	title: "ПІДКЛЮЧЕННЯ МАГАЗИНІВ",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Підключено",
	checking: "Перевірка...",
	authenticate: "Увійти",
	legendaryNotInstalled: "⚠️ Встановлення утиліти Epic Games...",
	nileNotInstalled: "⚠️ Утиліту Amazon Games не знайдено",
	error: "Помилка — перевірте журнали"
};
var librarySync$6 = {
	title: "СИНХРОНІЗАЦІЯ БІБЛІОТЕКИ",
	syncLibraries: "Синхронізувати бібліотеки",
	forceSync: "Примусова синхронізація",
	syncing: "Синхронізація...",
	cancelSync: "Скасувати синхронізацію",
	retryStatusCheck: "Повторити перевірку статусу",
	gamesSynced: "Синхронізовано ігор: {{synced}} / {{total}}",
	artworkDownloaded: "Завантажено обкладинок: {{synced}} / {{total}}",
	metadataExtracted: "Вилучено метаданих: {{synced}} / {{total}}",
	steamMetadataDownloaded: "Метадані Steam: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "Метадані unifiDB: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Дані Metacritic: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "Метадані RAWG: {{synced}} / {{total}}",
	compatChecked: "Дані сумісності: {{synced}} / {{total}}",
	cancelling: "Скасування…",
	error: "Помилка"
};
var cleanup$6 = {
	title: "Очищення",
	deleteAll: "Видалити всі бібліотеки та кеш UNIFIDECK",
	modalTitle: "Видалити всі дані Unifideck?",
	modalDescription: "Це видалить усі ярлики Unifideck, обкладинки Steam, збережені авторизації магазинів та дані кешу. Встановлені ігри залишаться на диску та можуть бути повторно синхронізовані. Увімкніть руйнівний режим нижче, щоб також видалити файли встановлених ігор.",
	deleteFilesLabel: "Також видалити файли встановлених ігор (Руйнівно)",
	deleteFilesDescription: "Видаляє папку встановлення на диску для кожної гри Unifideck у безпечному кореневому каталозі.",
	confirmDelete: "Так, видалити все",
	cancel: "Скасувати",
	deleting: "Видалення..."
};
var captureLogs$6 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (uk-UA).",
	title: "Збір журналів",
	description: "Збирає всі журнали Unifideck в один ZIP-архів у теці «Завантаження», щоб ви могли додати його до звіту про помилку.",
	capture: "Зібрати журнали",
	capturing: "Збір журналів...",
	savedTo: "Збережено до теки «Завантаження»",
	bundleInfo: "{{files}} файлів · {{size}}",
	partialNote: "Не вдалося прочитати {{count}} файл. Архів усе одно придатний.",
	partialNote_other: "Не вдалося прочитати {{count}} файлів. Архів усе одно придатний.",
	copyPath: "Копіювати шлях",
	modalTitle: "Створити діагностичний архів?",
	modalDescription: "Unifideck збере свої журнали в один ZIP-файл у теці «Завантаження», щоб ви могли додати його до звіту про помилку.",
	modalIncludes: "Включено: журнали плагіна та запусків ігор, історію синхронізації й встановлень, назви ваших ігор і шляхи встановлення, а також дані про пристрій — модель, операційну систему та накопичувачі.",
	modalExcludes: "Ніколи не включається: ваші пароли, токени входу до магазинів і файли cookie браузера.",
	modalConfirm: "Створити архів",
	cancel: "Скасувати"
};
var installButton$6 = {
	play: "Грати",
	resume: "Продовжити",
	install: "Встановити {{title}}",
	installNative: "Встановити",
	uninstall: "Видалити {{title}}",
	cancel: "✖ Скасувати",
	processing: "Обробка...",
	checking: "Перевірка...",
	size: "{{size}} ГБ",
	update: "ОНОВИТИ",
	updateAvailable: "ДОСТУПНЕ ОНОВЛЕННЯ",
	updateAvailableDescription: "Нова версія готова до встановлення."
};
var confirmModals$6 = {
	installTitle: "Підтвердження встановлення",
	installDescription: "Ви впевнені, що хочете встановити {{title}}?",
	uninstallTitle: "Підтвердження видалення",
	uninstallDescription: "Ви впевнені, що хочете видалити {{title}}?",
	cancelTitle: "Підтвердження скасування",
	cancelDescription: "Ви впевнені, що хочете скасувати завантаження {{title}}?",
	forceSyncTitle: "Параметри примусової синхронізації",
	forceSyncDescription: "Ви хочете заново синхронізувати всі обкладинки? Це перезапише будь-які ваші ручні зміни обкладинок. Виберіть 'Зберегти обкладинки', щоб завантажити тільки ті, яких бракує.",
	resyncArtwork: "Перезаписати обкладинки",
	keepArtwork: "Зберегти обкладинки",
	steamRestartTitle: "Синхронізацію завершено!",
	steamRestartDescription: "Будь ласка, перезапустіть Steam, щоб нові ігри та обкладинки з'явилися у бібліотеці",
	steamRestartDescriptionStore: "Будь ласка, перезапустіть Steam, щоб нові ігри та обкладинки {{store}} з'явилися у бібліотеці",
	steamRestartCleanupTitle: "Очищення завершено!",
	steamRestartCleanupDescription: "Будь ласка, перезапустіть Steam, щоб повністю прибрати видалені ігри та обкладинки",
	restartNow: "Перезапустити зараз",
	later: "Пізніше",
	ubisoftInstallTitle: "Необхідний Ubisoft Connect",
	ubisoftInstallDescription: "Це запустить програму встановлення Ubisoft Connect, яка необхідна для роботи {{title}}.",
	ubisoftInstallConfirm: "Відкрити Ubisoft Connect",
	yes: "Так",
	no: "Ні",
	amazonUpdateTitle: "Необхідне повне перевантаження",
	amazonUpdateDescription: "Оновлення Amazon вимагають повного повторного завантаження гри {{title}}. Це може зайняти деякий час. Продовжити?",
	updateConfirm: "Оновити"
};
var toasts$6 = {
	downloadStarted: "Завантаження розпочато. Перевірте UNIFIDECK > Завантаження",
	downloadQueued: "Завантаження в черзі",
	downloadQueuedBody: "Гру {{title}} додано до черги.",
	downloadFailedUnknown: "Невідома помилка.",
	updateQueued: "Оновлення в черзі",
	uninstallDone: "Видалення завершено.",
	ubisoftLauncherOpening: "Запуск Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "Ubisoft Connect запускається для {{title}}. Встановіть гру там, а потім поверніться, щоб грати.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect вже запущено для цього встановлення.",
	ubisoftLauncherOpenFailed: "Не вдалося запустити Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "Не вдалося запустити Ubisoft Connect для встановлення.",
	multipartDetected: "Виявлено багатокомпонентне завантаження",
	multipartMessage: "Будь ласка, зачекайте завершення процесу",
	downloadFailed: "Завантаження не вдалося",
	downloadFailedMessage: "Не вдалося розпочати завантаження.",
	downloadCancelled: "Завантаження скасовано",
	downloadCancelledMessage: "Завантаження {{title}} скасовано.",
	cancelFailed: "Не вдалося скасувати",
	cancelFailedMessage: "Не вдалося скасувати завантаження.",
	uninstalling: "Видалення...",
	uninstallingMessage: "Видалення {{title}}...",
	uninstallingMessageProton: "Видалення {{title}} та файлів Proton...",
	uninstallComplete: "Видалення завершено",
	uninstallCompleteMessage: "Гру {{title}} видалено.",
	uninstallCompleteMessageProton: "Гру {{title}} видалено (включаючи файли Proton).",
	installComplete: "Встановлення завершено!",
	installCompleteMessage: "Гра {{title}} готова до запуску.",
	syncComplete: "Синхронізацію завершено!",
	syncCompleteMessage: "Додано ігор: {{count}}. ПЕРЕЗАПУСТІТЬ STEAM (вийдіть повністю, а не просто поверніться в режим гри), щоб побачити їх у бібліотеці.",
	forceSyncComplete: "Примусову синхронізацію завершено!",
	forceSyncCompleteMessage: "Оновлено ігор: {{count}}. ПЕРЕЗАПУСТІТЬ STEAM для застосування змін.",
	syncCancelled: "Синхронізацію скасовано",
	authTimeout: "Час очікування авторизації минув",
	authTimeoutMessage: "Час авторизації у {{store}} минув. Будь ласка, спробуйте знову.",
	authFailed: "Авторизація не вдалася",
	authFailedMessage: "Не вдалося розпочати авторизацію",
	authError: "Помилка авторизації",
	authConnected: "Магазин {{store}} підключено!",
	authConnectedMessage: "Успішна авторизація в {{store}}",
	cleanupStarted: "Очищення запущено",
	cleanupStartedMessage: "Видалення ярликів, обкладинок та збережених облікових даних. Це може зайняти деякий час…",
	cleanupSuccessful: "Очищення успішно завершено",
	cleanupSuccessfulMessage: "Видалено ярликів: {{games}}, файлів обкладинок: {{artwork}}, виконано вихід з магазинів: {{stores}}, видалено папок встановлення: {{files}}. Кеш очищено.",
	cleanupNoop: "Нічого чистити",
	cleanupNoopMessage: "У Unifideck немає ярликів, обкладинок або збережених облікових даних для видалення.",
	deleteFailed: "Не вдалося видалити",
	repairPrefixSuccess: "Префікс виправлено",
	repairPrefixSuccessMessage: "Префікс Ubisoft Connect успішно перезібрано",
	repairPrefixFailed: "Не вдалося виправити",
	repairPrefixFailedMessage: "Не вдалося виправити префікс: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Помилка мережі",
		launchRetry: "Повторний запуск",
		retryingUmu: "Повторна спроба запуску UMU через {{seconds}} сек. (спроба {{attempt}}/{{max}})...",
		protonUpgrade: "Оновлення Proton",
		resettingPrefix: "Скидання префіксу для {{version}}...",
		firstTimeSetup: "Первинне налаштування",
		initializingPrefix: "Ініціалізація префіксу Wine...",
		setupCompleteTitle: "Налаштування завершено",
		prefixInitialized: "Префікс Wine ініціалізовано",
		setupFallback: "Налаштування",
		fallbackInitialization: "Резервна ініціалізація...",
		launchingGame: "Запуск гри",
		startingEpicGame: "Запуск гри Epic...",
		startingAmazonGame: "Запуск гри Amazon...",
		startingGogGame: "Запуск гри GOG...",
		startingUbisoftGame: "Запуск гри Ubisoft...",
		dependenciesTitle: "Встановлення залежностей",
		installingRedistributables: "Встановлення розповсюджуваних компонентів гри...",
		dependenciesReady: "Залежності готові",
		redistributablesInstalled: "Компоненти гри встановлено",
		dependenciesStatus: "Залежності",
		checkLogs: "Налаштування завершено (деталі див. у журналі)",
		installingProton: "Встановлення Proton",
		attemptingInstall: "Спроба встановити {{version}}...",
		epicAuthRequired: "Необхідна авторизація Epic",
		epicAuthRequiredMessage: "Будь ласка, увійдіть в Epic Games в налаштуваннях Unifideck",
		installingRedist: "Встановлення бібліотек",
		installingRedistMessage: "Запуск початкового встановлення бібліотек GOG...",
		setupFailed: "Налаштування не вдалося",
		setupFailedMessage: "Встановлення не вдалося, подробиці дивіться у логах",
		prerequisitesTitle: "Встановлення компонентів",
		installingPrerequisites: "Встановлення необхідних компонентів гри...",
		prerequisitesReady: "Компоненти готові",
		prerequisitesInstalled: "Компоненти гри успішно встановлено",
		prerequisitesFailed: "Попередження про компоненти",
		prerequisitesFailedMessage: "Деякі компоненти гри могли бути встановлені некоректно",
		browserRequired: "Необхідний Microsoft Edge",
		signingIn: "Вхід у систему...",
		cloudSyncDownFailed: "Не вдалося завантажити збереження з хмари для {{store}} ({{error}}). Гра запущена з локальними збереженнями.",
		cloudSyncUpFailed: "Не вдалося завантажити збереження в хмару для {{store}} ({{error}}). Ваш прогрес збережено лише локально.",
		errorCircuitBreakerOpen: "Запуск {{game_key}} не вдався {{count}} разів поспіль. Автоматичні запуски призупинено.",
		launcherError: "Не вдалося запустити {{game_key}} ({{error_code}}). Подробиці дивіться у логах.",
		downloadingProton: "Завантаження Proton — це може зайняти кілька хвилин...",
		downloadingRuntime: "Завантаження Steam Runtime — перший запуск може зайняти кілька хвилин...",
		installingUbisoft: "Встановлення через Ubisoft Connect",
		installingUbisoftMessage: "Відкриття Ubisoft Connect — почніть встановлення у вікні лаунчера.",
		protonReadyBody: "{{version}} готовий до використання",
		protonReadyTitle: "Proton готовий",
		protonSwitchedTo: "Тепер використовується {{version}}",
		signingInUbisoft: "Вхід у Ubisoft Connect",
		signingInUbisoftMessage: "Відкриття Ubisoft Connect — увійдіть там, а потім поверніться.",
		ubisoftPrefixNotReady: "Ubisoft Connect не готовий",
		ubisoftPrefixNotReadyMessage: "Не вдалося підготувати префікс Ubisoft Connect для цієї гри. Спробуйте встановити ще раз; якщо помилка повторюється, повторно запустіть синхронізацію бібліотеки.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect не готовий",
		ubisoftAuthPrefixNotReadyMessage: "Не вдалося підготувати Ubisoft Connect для входу. Спробуйте ще раз через мить.",
		ubisoftLaunchIdMissing: "Відкриття Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "Не вдалося визначити ID запуску цієї гри — відкривається Ubisoft Connect, щоб ви могли запустити її з бібліотеки. Повторно запустіть синхронізацію бібліотеки, щоб виправити пряме запускання."
	},
	updateComplete: "Оновлення завершено!",
	updateCompleteMessage: "{{title}} оновлено.",
	updateCancelled: "Оновлення скасовано",
	updateCancelledMessage: "Оновлення {{title}} було скасовано.",
	updateFailed: "Оновлення не вдалося",
	updateFailedMessage: "Не вдалося розпочати оновлення.",
	storeError: "Не вдалося зв'язатися з магазином {{store}} ({{errType}})",
	captureLogsStarted: "Збір журналів...",
	captureLogsStartedMessage: "Створення діагностичного архіву в теці «Завантаження». Це триває кілька секунд.",
	captureLogsDone: "Журнали зібрано",
	captureLogsDoneMessage: "Збережено до {{path}} ({{size}}, файлів: {{files}}).",
	captureLogsPartial: "Журнали зібрано (частково)",
	captureLogsPartialMessage: "Збережено до {{path}} ({{size}}, файлів: {{files}}). Не вдалося прочитати файлів: {{failures}}.",
	captureLogsFailed: "Не вдалося зібрати журнали",
	captureLogsBusy: "Збір журналів уже виконується",
	captureLogsCopied: "Шлях скопійовано",
	captureLogsCopyFailed: "Не вдалося скопіювати шлях",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Звільнити місце",
		signInToStore: "Увійти",
		retrySync: "Повторити",
		retryLibrarySync: "Оновити бібліотеку",
		openSaveFolder: "Відкрити папку збережень",
		showLogs: "Показати логи"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "Не вдалося оновити бібліотеку магазину {{store}} ({{error}}). Поточний список ігор залишиться без змін."
	},
	cloudPullStarted: "Скачування хмарного збереження…",
	cloudPushStarted: "Вивантаження локального збереження…",
	cloudSavePathFailed: "Не вдалося встановити розташування збережень",
	cloudSavePathSet: "Розташування збережень оновлено",
	cloudPullDone: "Хмарне збереження завантажено",
	cloudPullFailed: "Не вдалося завантажити хмарне збереження",
	cloudPushDone: "Локальне збереження вивантажено",
	cloudPushFailed: "Не вдалося вивантажити локальне збереження"
};
var uninstallModal$6 = {
	title: "Підтвердження видалення",
	description: "Ви впевнені, що хочете видалити {{title}}?",
	deleteProtonLabel: "Видалити також файли Proton",
	deleteProtonDescription: "Видаляє дані сумісності та файли користувача",
	warningTitle: "Попередження:",
	warningBody: "Це назавжди видалить збереження гри, що зберігаються всередині префіксу Wine. Переконайтеся, що у вас є резервна копія.",
	cancel: "Скасувати",
	uninstall: "Видалити"
};
var languageSettings$6 = {
	title: "МОВА",
	label: "Мова інтерфейсу",
	description: "Мова інтерфейсу та завантажуваних ігор (якщо підтримується)",
	autoDetect: "Авто (Системна)"
};
var errors$6 = {
	legendaryNotFound: "Утиліту Epic Games CLI (Legendary) не знайдено",
	gogdlNotFound: "Утиліту GOG CLI (gogdl) не знайдено",
	nileNotFound: "Утиліту Amazon Games CLI (Nile) не знайдено",
	authUrlFailed: "Не вдалося отримати посилання для авторизації",
	authFailed: "Авторизація не вдалася",
	notAuthenticated: "Не авторизовано. Будь ласка, спочатку увійдіть у систему.",
	syncInProgress: "Синхронізація вже виконується",
	syncCancelled: "Синхронізація скасована користувачем",
	gameNotFound: "Гра не знайдена",
	gameNotInstalled: "Гра не встановлена",
	installFailed: "Встановлення завершилося помилкою — подробиці див. у логах",
	installNoDirectory: "Встановлення завершено, але не вдалося знайти папку гри",
	uninstallFailed: "Не вдалося видалити гру",
	lockConflict: "Конфлікт блокування — будь ласка, спробуйте пізніше",
	unknownStore: "Невідомий магазин",
	shortcutWriteFailed: "Не вдалося записати ярлики",
	backgroundSyncDisabled: "Фонова синхронізація вимкнена",
	steamGridDbUnavailable: "Бібліотека SteamGridDB недоступна",
	deleteSyncInProgress: "Не можна проводити видалення під час синхронізації",
	alreadyInQueue: "Вже у черзі",
	download: {
		diskSpace: "Недостатньо вільного місця на диску",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Помилка мережі — перевірте підключення до Інтернету",
		authExpired: "Сесія авторизації закінчилася — будь ласка, увійдіть знову",
		toolMissing: "Необхідна утиліта завантаження не знайдена",
		processFailed: "Завантаження перервалося — будь ласка, спробуйте ще раз",
		directoryNotFound: "Не вдалося знайти встановлені файли гри",
		lockConflict: "Виконується інша операція — спробуйте пізніше",
		ubisoftClient: "Помилка клієнта Ubisoft Connect — спробуйте виправити префікс",
		ubisoftClientNotRunning: "Клієнт Ubisoft Connect не відповідає",
		ubisoftBootstrap: "Не вдалося налаштувати префікс Ubisoft Connect",
		ubisoftUmuMissing: "Не знайдено інструмент umu-run — необхідний для ігор Ubisoft",
		generic: "Встановлення завершилося помилкою — подробиці див. у логах"
	},
	unknown: "Невідома помилка",
	steamAppsUnavailable: "API SteamClient.Apps недоступне"
};
var sync$6 = {
	start: "Синхронізувати бібліотеки",
	force: "Примусова синхронізація",
	syncing: "Синхронізація…",
	cancel: "Скасувати синхронізацію",
	cancelling: "Скасування…",
	preparing: "Підготовка…",
	downloadingArtwork: "Завантаження обкладинки для {{game}}...",
	fetchingGameLists: "Отримання списку ігор...",
	fetchingStore: "Отримання ігор {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "Синхронізацію скасовано користувачем",
	checkingInstalledGames: "Перевірка встановлених ігор...",
	sgdbLookup: "Пошук {{count}} ігор на SteamGridDB...",
	extractingSteamMetadata: "Вилучення метаданих Steam...",
	fetchingEnhancedMetadata: "Отримання розширених метаданих...",
	lookingUpUnifiDB: "Отримання метаданих unifiDB...",
	fetchingMetacriticData: "Отримання метаданих Metacritic...",
	fetchingCompatData: "Перевірка сумісності...",
	checkingExistingArtwork: "Перевірка існуючих обкладинок...",
	cleaningOrphanedArtwork: "Очищення невикористовуваних обкладинок...",
	retryingMissingArtwork: "Повторна спроба для {{synced}} / {{total}} ігор без обкладинок...",
	savingShortcuts: "Збереження ярликів...",
	completed: "Синхронізацію завершено!",
	extractingMetadata: "Вилучення метаданих для {{game}}...",
	cooldown: "Зачекайте {{secs}} сек.",
	artworkPhase: "Обкладинка {{done}} / {{total}}"
};
var force_sync$6 = {
	migratingOldInstallations: "Примусова синхронізація: Перенесення старих встановлень...",
	fetchingGameLists: "Примусова синхронізація: Отримання списків ігор...",
	checkingInstalledGames: "Примусова синхронізація: Перевірка встановлених ігор...",
	rewritingShortcuts: "Примусова синхронізація: Перезапис ярликів...",
	cancelledShortcutsSaved: "Примусову синхронізацію скасовано — ярлики збережено",
	writingShortcuts: "Примусова синхронізація: Запис ярликів...",
	clearingProton: "Примусова синхронізація: Очищення префіксів сумісності Proton...",
	completed: "Примусову синхронізацію завершено! Оновлено ігор: {{updated}}, завантажено обкладинок: {{artwork}}."
};
var artwork$6 = {
	checking: "Перевірка обкладинок...",
	queueRefresh: "Додавання всіх ігор у чергу на оновлення обкладинок...",
	downloadProgress: "Завантажено обкладинок: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$6 = {
	greatOnDeck: "Чудово на Deck",
	allGames: "Усі ігри",
	installed: "Встановлено",
	nonSteam: "Сторонні ігри",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$6 = {
	error: "Помилка Unifideck",
	errorLoadingView: "Не вдалося завантажити єдину бібліотеку ігор",
	checkConsole: "Подробиці дивіться в консолі браузера",
	errorLoadingGames: "Помилка під час завантаження списку ігор",
	tryReloading: "Спробуйте оновити плагін або подивіться консоль",
	storeLabel: "Магазин:",
	allStores: "Усі магазини",
	searchPlaceholder: "Пошук ігор...",
	library: "Бібліотека"
};
var gameInfoPanel$6 = {
	loading: "Завантаження інформації про гру...",
	noMetadata: "Немає доступних метаданих для цієї гри.",
	noTestResults: "Немає детальних результатів тестування.",
	buttons: {
		details: "Подробиці",
		synopsis: "Опис",
		storePage: "Сторінка магазину",
		dlc: "DLC",
		communityHub: "Центр спільноти",
		pointsShop: "Магазин очок",
		discussions: "Обговорення",
		guides: "Посібники",
		support: "Підтримка",
		install: "Встановити",
		uninstall: "Видалити",
		cancel: "Скасувати",
		achievements: "Досягнення"
	},
	labels: {
		store: "Магазин:",
		name: "Назва:",
		size: "Розмір:",
		developer: "Розробник:",
		publisher: "Видавець:",
		released: "Випущено:",
		metacritic: "Metacritic:",
		status: "Статус:",
		spaceRequired: "Необхідно місця",
		lastPlayed: "Останній запуск",
		playTime: "Час у грі",
		never: "Ніколи"
	},
	compatibility: {
		modalTitle: "Сумісність зі Steam Deck",
		viewOnProtonDb: "Подивитися на ProtonDB",
		close: "Закрити",
		verified: "СУМІСНО",
		playable: "МОЖНА ГРАТИ",
		unsupported: "НЕ ПІДТРИМУЄТЬСЯ",
		unknown: "НЕВІДОМО"
	},
	failedToLoadMetadata: "Не вдалося завантажити метадані",
	lastSession: "Минула сесія: розблоковано {{names}}"
};
var gameDetailsSettings$6 = {
	title: "ВИД СТОРІНКИ ГРИ",
	simple: "Простий",
	detailed: "Детальний"
};
var gogLanguageModal$6 = {
	title: "Вибрати мову",
	description: "Виберіть мову завантаження для гри {{title}}",
	label: "Мова",
	install: "Встановити",
	cancel: "Скасувати"
};
var accountSwitch$6 = {
	title: "Виявлено зміну акаунта",
	description: "Ви увійшли під іншим акаунтом Steam. Що ви хочете зробити з даними Unifideck?",
	migrate: "Перенести дані Unifideck",
	migrateDescription: "Копіювати ігри та обкладинки з попереднього акаунта",
	freshStart: "Почати заново",
	freshStartDescription: "Очистити авторизацию магазинів для підключення інших акаунтів Epic/GOG/Amazon/Ubisoft",
	skip: "Пропустити",
	toastMigrateTitle: "Перенесення завершено",
	toastMigrateBody: "Перенесено ярликів: {{shortcuts}}, обкладинок: {{artwork}}",
	toastClearTitle: "Токени авторизації видалено",
	toastClearBody: "Авторизацію в магазинах було скинуто. Тепер ви можете увійти під іншими акаунтами."
};
var ubisoftAuth$6 = {
	title: "Вхід у Ubisoft Connect",
	description: "Увійдіть в Ubisoft Connect, щоб прив'язати вашу бібліотеку.",
	signInViaUpc: "Увійти через Ubisoft Connect",
	upcDescription: "Ubisoft Connect відкриється для входу. Після успішного входу сесію буде автоматично імпортовано.",
	launchingUpc: "Запуск Ubisoft Connect...",
	waitingForLogin: "Очікування входу...",
	retryLaunch: "Повторити спробу",
	cancel: "Скасувати",
	launchFailed: "Не вдалося запустити Ubisoft Connect. Будь ласка, спробуйте ще раз.",
	signingIn: "Вхід...",
	emailLabel: "Електронна пошта",
	passwordLabel: "Пароль",
	signIn: "Увійти",
	twoFactorTitle: "Двофакторна автентифікація",
	twoFactorDescription: "Введіть код перевірки з вашого додатка-автентифікатора.",
	twoFactorLabel: "Код перевірки",
	verify: "Перевірити",
	verifying: "Перевірка...",
	errorEmptyFields: "Будь ласка, введіть пошту та пароль",
	errorEmpty2FA: "Будь ласка, введіть код перевірки",
	errorLoginFailed: "Не вдалося увійти. Будь ласка, перевірте облікові дані.",
	errorInvalid2FA: "Невірний код перевірки. Будь ласка, спробуйте ще раз.",
	showPassword: "Показати пароль",
	hidePassword: "Приховати пароль",
	doubleLoginWarning: "Вам потрібно буде увійти в Ubisoft двічі."
};
var relativeDate$6 = {
	today: "Сьогодні",
	yesterday: "Вчора"
};
var common$6 = {
	thisGame: "цю гру",
	game: "Гра",
	unknown: "Невідомо",
	ok: "ОК",
	cancel: "Скасувати",
	working: "Обробка…",
	loading: "Завантаження…",
	free: "безкоштовно",
	close: "Закрити"
};
var info$6 = {
	expandDetails: "Розгорнути",
	collapseDetails: "Згорнути"
};
var microsoft$6 = {
	notCompatible: "Цей формат гри несумісний зі SteamOS",
	playOnCloud: "Грати в хмарі",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Увійдіть зі своїм обліковим записом Microsoft / Xbox",
	accountConnected: "Обліковий запис Microsoft підключено",
	loggedOut: "Вихід з Microsoft Store виконано",
	xcloudNotInstalled: "Ігри xCloud не встановлюються локально",
	chromiumRequired: "Необхідний Microsoft Edge",
	chromiumRequiredMessage: "Для входу потрібен Microsoft Edge. Встановити його автоматично? (Це потрібно зробити лише один раз).",
	chromiumInstallButton: "Встановити Microsoft Edge",
	chromiumInstalling: "Встановлення Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge встановлено!",
	chromiumAlreadyInstalled: "Сумісний браузер вже встановлено",
	chromiumInstallFailed: "Не вдалося встановити Microsoft Edge. Будь ласка, встановіть вручну: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Перевищено час очікування встановлення Microsoft Edge. Перевірте підключення до Інтернету.",
	browserAlreadyInstalled: "Сумісний браузер вже встановлено",
	browserInstalled: "Microsoft Edge успішно встановлено!",
	browserInstallFailed: "Не вдалося встановити Microsoft Edge. Будь ласка, встановіть вручну: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak недоступний у цій системі",
	signInRequired: "Увійдіть, щоб грати",
	syncSkippedNoSubscription: "Активну підписку Xbox Game Pass не знайдено. Вашу бібліотеку xCloud не синхронізовано.",
	syncSkippedTierUnknown: "Підписку Xbox Game Pass виявлено, але рівень визначити не вдалося. Синхронізацію пропущено, щоб уникнути показу недоступних ігор.",
	subscriptionCheckFailed: "Не вдалося перевірити підписку Xbox Game Pass. Синхронізацію xCloud пропущено. Спробуйте ще раз, коли підключення стане стабільним.",
	subscriptionDetected: "Виявлено активну підписку Xbox Game Pass {{tier}}."
};
var playButton$6 = {
	play: "Грати",
	install: "Встановити",
	cancel: "Скасувати",
	retry: "Повторити",
	downloadFailed: "Завантаження не вдалося",
	installing: "Встановлення",
	controllerConfig: "Налаштування контролера",
	appSettings: "Налаштування додатка"
};
var play$6 = {
	play: "Грати",
	resume: "Продовжити",
	stop: "Зупинити",
	cancel: "Скасувати",
	cancelling: "Скасування…",
	update: "Оновити",
	uninstall: "Видалити",
	queued: "У черзі",
	downloading: "Завантаження",
	extracting: "Розпакування",
	verifying: "Перевірка",
	finalizing: "Завершення встановлення",
	cancelConfirmTitle: "Скасувати завантаження?",
	cancelConfirmBody: "Ви впевнені, що хочете скасувати завантаження {{title}}? Частково завантажені файли будуть видалені.",
	cancelConfirmConfirm: "Скасувати завантаження",
	cancelConfirmCancel: "Продовжити завантаження",
	playOnCloud: "Грати в хмарі",
	cloudSave: {
		autoPullOn: "Хмарні збереження завантажуються автоматично при запуску.",
		close: "Закрити",
		download: "Завантажити хмарне збереження",
		filesCount: "{{count}} файлів",
		label: "Хмарні збереження",
		menuTitle: "Хмарні збереження — {{game}}",
		noCloudData: "Невідомо",
		noCloudSupport: "Немає підтримки хмарних збережень",
		setLocation: "Встановити розташування збережень",
		setLocationHint: "Виберіть папку, у якій ця гра зберігає свої збереження.",
		setLocationTitle: "Виберіть папку збережень цієї гри",
		statusCloudAvailable: "Хмарне збереження доступне",
		statusNoSaves: "Немає локальних збережень",
		statusSyncing: "Синхронізація хмарних збережень…",
		statusUnresolved: "Розташування збережень не знайдено — синхронізація може не працювати для цієї гри.",
		unresolvedHint: "Запустіть гру один раз, щоб ми могли виявити її папку збережень, або встановіть розташування вручну.",
		upload: "Завантажити локальне збереження"
	},
	exe: {
		browse: "Огляд…",
		changeFailed: "Не вдалося змінити виконуваний файл",
		changed: "Виконуваний файл змінено",
		defaultTag: "(за замовчуванням)",
		menuItem: "Змінити виконуваний файл…",
		reset: "Скинути на виконуваний файл за замовчуванням",
		resetButton: "Скинути до стандартного",
		subtitle: "Виберіть, який файл запускає цю гру (наприклад, щоб обійти лаунчер і запустити гру напряму). Робоча папка та хмарні збереження не зміняться.",
		title: "Змінити виконуваний файл — {{game}}",
		unavailable: "Не вдалося прочитати папку цієї гри."
	}
};
var playMeta$6 = {
	spaceRequired: "Необхідно місця",
	installedSize: "Розмір на диску",
	lastPlayed: "Останній запуск",
	neverPlayed: "Жодного разу не запускалася",
	hoursShort: "год.",
	minutesShort: "хв.",
	played: "Зіграно"
};
var downloads$6 = {
	title: "Завантаження",
	empty: "Завантажень немає. Встановіть ігри зі своєї бібліотеки, щоб побачити їх тут.",
	current: "Завантажується зараз",
	queued: "У черзі",
	finished: "Нещодавно завершені",
	cancel: "Скасувати",
	removeFromQueue: "Видалити",
	play: "Грати",
	outcome: {
		queued: "У черзі",
		downloading: "Завантаження",
		completed: "Завершено",
		cancelled: "Скасовано",
		error: "Помилка"
	}
};
var navigation$6 = {
	loading: "Завантаження...",
	storesTitle: "Магазини",
	syncTitle: "Синхр. бібліотеки",
	languageTitle: "Мова"
};
var security$6 = {
	panelTitle: "Безпека",
	counters: {
		label: "Лічильники",
		empty: "Подій з моменту запуску не виявлено"
	},
	log: {
		label: "Журнал аудиту",
		entryCount: "{{count}} запис",
		entryCount_other: "{{count}} записів",
		emptyFiltered: "Подій, що відповідають фільтру, не виявлено"
	},
	filter: {
		label: "Фільтр",
		all: "Усі події"
	},
	timeline: {
		label: "Хроніка за 24 год ({{count}} подія)",
		label_other: "Хроніка за 24 год ({{count}} подій)",
		axisStart: "-24 год",
		axisEnd: "зараз"
	},
	relativeTime: {
		seconds: "{{count}} сек. тому",
		minutes: "{{count}} хв. тому",
		hours: "{{count}} год. тому",
		days: "{{count}} дн. тому"
	},
	bruteforce: {
		warningTitle: "Попередження: виявлено підбір паролів (рівень попередження)",
		escalationTitle: "АЛАРМ: виявлено підбір паролів (рівень ескалації)",
		detail: "{{failures}} невдалих спроб розшифрування за останні {{window}} сек. Пороги: {{warning}} (попередження) / {{escalation}} (ескалація).",
		resetButton: "Підтвердити та скинути детектор"
	},
	actions: {
		clearLog: "Очистити журнал та лічильники"
	},
	events: {
		tokenEncrypted: "Токен зашифровано",
		tokenDecrypted: "Токен розшифровано",
		decryptFailed: "Не вдалося розшифрувати",
		tokenFileMigrated: "Файл токенів перенесено",
		legacyPlaintextDetected: "Виявлено застарілий відкритий текст",
		authFlowStarted: "Авторизацію запущено",
		authFlowCompleted: "Авторизацію завершено",
		authFlowFailed: "Авторизація не вдалася",
		permissionsCheck: "Права доступу перевірено",
		permissionsRepaired: "Права доступу відновлено",
		bruteforceSuspected: "Підозра на підбір паролів",
		deviceResetDetected: "Виявлено скидання пристрою",
		fingerprintInitialized: "Відбиток ініціалізовано",
		externalAuthCheckFailed: "Перевірка зовнішньої авторизації завершилася помилкою",
		configValidationCompleted: "Перевірку конфігурації завершено",
		configValidationFailed: "Перевірка конфігурації не вдалася"
	},
	configValidation: {
		degradedTitle: "Невірна конфігурація: активний режим обмеженої функціональності",
		detail: "Виявлено {{count}} помилку конфігурації під час запуску",
		detail_other: "Виявлено {{count}} помилок конфігурації під час запуску",
		sourceDefaults: "значення за замовчуванням",
		sourceUser: "налаштування користувача",
		sourceMerged: "об'єднані",
		overflow: "+ {{count}} інша помилка",
		overflow_other: "+ {{count}} інших помилок"
	}
};
var cloudSyncBehavior$6 = {
	_comment: "Панель налаштувань, що дозволяє користувачеві вибрати для кожного магазину, як показувати помилки синхронізації збережень. П'ять записів store_label відповідають рядкам, що відображаються CloudSyncBehaviorPanel.tsx. Microsoft навмисно опущена: хмарні ігри xCloud не мають локальних збережень.",
	title: "Синхронізація збережень",
	description: "Виберіть поведінку Unifideck у разі збою завантаження або надсилання хмарних збережень під час запуску гри.",
	loadError: "Не вдалося завантажити поточні налаштування",
	saveError: "Не вдалося зберегти зміни. Будь ласка, спробуйте ще раз",
	store_label: {
		"default": "За замовчуванням (усі магазини)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Тихий режим (тільки в журнал)",
		toast: "Повідомляти (спливаюче повідомлення)"
	}
};
var cloudSync$6 = {
	_comment: "Спливаючі повідомлення, що надсилаються лаунчером у разі сбою хмарної синхронізації. Опис помилки зіставляється через cloudSync.error.<code> з використанням поля error_i18n_key з відповіді бекенду.",
	error: {
		network_unreachable: "Мережа недоступна",
		auth_expired: "Термін сесії авторизації закінчився",
		forbidden: "Доступ заборонено",
		quota_exceeded: "Перевищено квоту хмарного сховища",
		server_error: "Сервер магазину недоступний",
		timed_out: "Час очікування запиту минув",
		disk_full: "Недостатньо місця на диску",
		disk_space_low: "Пропущено: мало місця на диску (звільніть місце та спробуйте знову)",
		permission_denied: "Доступ обмежено",
		cancelled: "Скасовано",
		unknown: "Непередбачена помилка"
	},
	unavailableNativeDep: "Хмарні збереження вимкнено на цій системі — у її Python ({{python}}) відсутній необхідний компонент. Це не впливає на ваші локальні збереження; перезберіть або перевстановіть плагін після оновлення системи, щоб відновити синхронізацію."
};
var launchLogs$6 = {
	_comment: "Повідомлення для LaunchLogsModal. Модальне вікно відкривається при переході за посиланням unifideck://show-logs/<launch_id>, прикріпленим до повідомлень про сбій лаунчера (circuit breaker + LauncherError). Читає архівні логи запуску з ~/.local/share/unifideck/launches/<launch_id>.log і виводить їх з кольоровою розміткою за важливістю.",
	title: "Логи запуску — {{launchId}}",
	close: "Закрити",
	copyAll: "Копіювати все",
	"export": "Експорт у файл",
	copied: "Скопійовано в буфер обміну",
	copyFailed: "Не вдалося скопіювати",
	exporting: "Експорт…",
	exportedTo: "Експортовано в {{path}}",
	exportFailed: "Помилка експорту: {{error}}",
	loading: "Завантаження логів…",
	notAvailable: "Логи цього запуску більше не доступні (минув 7-денний термін зберігання).",
	error: "Не вдалося прочитати логи: {{error}}",
	fileInfo: "{{path}} — показано {{shown}} з {{total}} рядків",
	filterLabel: "Показати",
	filterInfo: "Інфо",
	filterWarning: "Попередження",
	filterError: "Помилка",
	nextError: "Наступна помилка ({{current}}/{{total}})",
	noErrors: "Помилок немає",
	allFiltered: "Усі рядки приховані поточним фільтром. Увімкніть хоча б один рівень важливості, щоб побачити записи."
};
var settings$6 = {
	_comment: "Елементи керування UnifideckSettingsPanel, що не увійшли до інших компонентів (LanguageSelector, CloudSyncBehaviorPanel). refreshAll — кнопка, що запускає sync_all() для всіх налаштованих магазинів; бібліотека виводить прогрес синхронізації через підписку на події SYNC_PROGRESS.",
	storeConnections: "Підключення магазинів",
	librarySync: "Синхр. бібліотеки",
	language: "Мова",
	languageAuto: "Визначати автоматично",
	refreshAll: {
		sectionTitle: "Дії з бібліотекою",
		button: "Оновити всі бібліотеки",
		buttonBusy: "Запуск…",
		startedTitle: "Оновлення бібліотеки запущено",
		startedBody: "Синхронізація всіх магазинів виконується у фоновому режимі.",
		startedSingle: "Оновлення бібліотеки {{store}} виконується у фоновому режимі.",
		failedTitle: "Не вдалося запустити оновлення бібліотеки"
	}
};
var saveFolder$6 = {
	_comment: "Повідомлення для SaveFolderModal. Модальне вікно відкривається при переході за посиланням unifideck://open-save-folder/<store>/<game_id>, як правило, при сбої хмарної синхронізації, коли користувач хоче переглянути локальні файли. count та size підставляються; truncated виводиться лише при обмеженні виведення списку.",
	title: "Папка збережень — {{store}} / {{gameId}}",
	close: "Закрити",
	copyPath: "Копіювати шлях",
	copyFileList: "Копіювати список файлів",
	depthLabel: "Глубина",
	filterLabel: "Фільтр",
	loading: "Завантаження вмісту папки…",
	noSavesYet: "Локальних збережень для цієї гри поки не знайдено. Папку буде створено при наступній успішній хмарній синхронізації.",
	aggregate: "Файлів: {{count}}, загальний обсяг: {{size}}",
	truncated: "Ще {{count}} файлів ({{size}}) не показано. Уточніть фільтр для перегляду.",
	error: "Не вдалося прочитати папку збережень: {{error}}"
};
var library$6 = {
	_comment: "Поігрові служби інтерфейсу в бібліотеці, включаючи індикатор автоматического вимикача запусків та елементи примусового запуску / скидання помилок при повторних збоях.",
	circuitBreaker: {
		_comment: "Інтерфейс для запобіжника сбоїв запуску. Індикатор з'являється, тільки якщо число помилок у заданому інтервалі досягає circuit_breaker.failures_threshold (за замовчуванням 3 за 600 сек). Примусовий запуск виконує одноразовий обхід захисту; Скидання очищає історію сбоїв.",
		badgeRecent: "{{count}} нещодавній збій запуску",
		badgeRecent_other: "{{count}} нещодавніх збоїв запуску",
		badgeTooltip: "{{count}} невдалих запусків за останній час. Запобіжник призупинив автоматичні запуски.",
		forceLaunchButton: "Запустити примусово",
		resetButton: "Скинути помилки",
		failureKind: {
			fastBoot: "Виліт при швидкому запуску",
			launcherError: "Помилка підготовки лаунчера"
		}
	}
};
var pickStorage$6 = {
	title: "Папка встановлення",
	customOption: "Вибрати іншу папку...",
	customLabel: "Користувацька папка",
	change: "Змінити...",
	gameSize: "Розмір встановлення"
};
var restart$6 = {
	title: "Синхронізацію завершено!",
	body: "Будь ласка, перезапустіть Steam для застосування ваших нових ігор та обкладинок",
	confirm: "Перезапустити зараз",
	later: "Пізніше"
};
var updater$6 = {
	checkButton: "Перевірити оновлення",
	checkCompleteMessage: "Інформацію про останню версію успішно отримано.",
	checkCompleteTitle: "Перевірку оновлень завершено",
	checkFailedTitle: "Помилка перевірки",
	checkingButton: "Перевірка...",
	currentTitle: "Поточна",
	downgradeButton: "Відкат до версії v{{version}}",
	installFailedTitle: "Встановлення не вдалося",
	installTimeoutMessage: "Немає відповіді від Decky Loader — можливо, встановлення зависло або не вдалося. Будь ласка, спробуйте ще раз.",
	installedLabel: "встановлено",
	installingButton: "Встановлення...",
	installingTitle: "Встановлення плагіна",
	latestLabel: "остання",
	modalTitle: "UNIFIDECK v{{version}} — Опис випуску",
	noBackend: "Бекенд Decky недоступний.",
	noReleaseNotes: "Опис змін відсутній.",
	reinstallButton: "Перевстановити версію v{{version}}",
	refreshFailedMessage: "Не вдалося перевірити інформацію про останню версію. Перевірте з'єднання та спробуйте ще раз.",
	releaseGoneMessage: "Цей випуск більше недоступний. Перевірте оновлення та виберіть знову.",
	releaseNotesButton: "Опис змін",
	stageDownload: "Завантаження…",
	stageFinish: "Завершення…",
	stageInstall: "Встановлення…",
	stageOpen: "Читання пакета…",
	stageParse: "Перевірка…",
	stageRemove: "Видалення старої версії…",
	stageStart: "Запуск…",
	stageWorking: "Встановлення…",
	titleLoading: "Перевірка версії",
	typeDowngrade: "Відкат до версії",
	typeReinstall: "Перевстановлення версії",
	typeUpdate: "Оновлення до версії",
	updateButton: "Оновити до v{{version}}"
};
var achievements$6 = {
	unlockedTitle: "Досягнення розблоковано",
	unlockedBody: "{{name}}"
};
var achievementsModal$6 = {
	title: "Досягнення",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Оновити",
	close: "Закрити",
	loading: "Завантаження досягнень…",
	empty: "У цій грі немає досягнень.",
	hidden: "Приховане досягнення",
	unlockedOn: "Розблоковано {{date}}",
	error: "Не вдалося завантажити досягнення.",
	errorOffline: "Підключіться до інтернету, щоб переглянути досягнення.",
	errorAuth: "Увійдіть у магазин цієї гри, щоб переглянути досягнення.",
	errorUnavailable: "Досягнення недоступні для цієї гри."
};
var collectionSettings$6 = {
	title: "КОЛЕКЦІЇ STEAM",
	enabled: "Колекції увімкнено",
	disabled: "Колекції вимкнено"
};
var ukUA = {
	_comment: _comment$6,
	tabs: tabs$6,
	auth: auth$6,
	storage: storage$6,
	storageSettings: storageSettings$6,
	gameGrid: gameGrid$6,
	downloadsTab: downloadsTab$6,
	cloudSave: cloudSave$6,
	storeConnections: storeConnections$6,
	librarySync: librarySync$6,
	cleanup: cleanup$6,
	captureLogs: captureLogs$6,
	installButton: installButton$6,
	confirmModals: confirmModals$6,
	toasts: toasts$6,
	uninstallModal: uninstallModal$6,
	languageSettings: languageSettings$6,
	errors: errors$6,
	sync: sync$6,
	force_sync: force_sync$6,
	artwork: artwork$6,
	deckTabs: deckTabs$6,
	unifiedLibrary: unifiedLibrary$6,
	gameInfoPanel: gameInfoPanel$6,
	gameDetailsSettings: gameDetailsSettings$6,
	gogLanguageModal: gogLanguageModal$6,
	accountSwitch: accountSwitch$6,
	ubisoftAuth: ubisoftAuth$6,
	relativeDate: relativeDate$6,
	common: common$6,
	info: info$6,
	microsoft: microsoft$6,
	playButton: playButton$6,
	play: play$6,
	playMeta: playMeta$6,
	downloads: downloads$6,
	navigation: navigation$6,
	security: security$6,
	cloudSyncBehavior: cloudSyncBehavior$6,
	cloudSync: cloudSync$6,
	launchLogs: launchLogs$6,
	settings: settings$6,
	saveFolder: saveFolder$6,
	library: library$6,
	pickStorage: pickStorage$6,
	restart: restart$6,
	updater: updater$6,
	achievements: achievements$6,
	achievementsModal: achievementsModal$6,
	collectionSettings: collectionSettings$6
};

var _comment$5 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$5 = {
	settings: "Ayarlar",
	downloads: "İndirmeler"
};
var auth$5 = {
	connect: "Bağlan",
	disconnect: "Bağlantıyı Kes",
	status: {
		connected: "Bağlı",
		disconnected: "Bağlı Değil",
		checking: "Kontrol ediliyor…",
		error: "Hata — günlükleri kontrol edin"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$5 = {
	title: "Depolama",
	defaultUpdated: "Varsayılan depolama güncellendi",
	isDefault: "Varsayılan",
	setDefault: "Varsayılan Yap",
	customPath: "Özel Yol",
	up: "↑ Yukarı",
	refresh: "Yenile",
	useThisPath: "Bu Yolu Kullan"
};
var storageSettings$5 = {
	title: "İndirme Ayarları",
	installLocationLabel: "Yükleme Konumu",
	installLocationDescription: "Yeni oyunların indirileceği yer",
	pathLabel: "Yol",
	loading: "Depolama seçenekleri yükleniyor...",
	toastUpdatedTitle: "Depolama Konumu Güncellendi",
	toastUpdatedBody: "Yeni oyunlar şuraya yüklenecek: {{location}}",
	toastFailedTitle: "Güncelleme Başarısız",
	toastFailedBody: "{{error}}",
	unknown: "Bilinmeyen",
	browseButton: "Göz at...",
	clearCustom: "Özel Konumu Temizle",
	customPathSet: "Özel yükleme konumu ayarlandı",
	newFolder: "Yeni Klasör",
	newFolderCreate: "Oluştur",
	selectFolder: "Bu Klasörü Kullan",
	emptyDirectory: "Boş klasör",
	showHidden: "Gizlileri Göster",
	selectDevice: "Cihaz Seç",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "Değiştirilme (En yeni)",
	sortModifiedOldest: "Değiştirilme (En eski)",
	sortCreatedNewest: "Oluşturulma (En yeni)",
	sortCreatedOldest: "Oluşturulma (En eski)",
	sortSizeLargest: "Boyut (En büyük)",
	sortSizeSmallest: "Boyut (En küçük)",
	"validationErrors.notAbsolute": "Yol mutlak olmalıdır",
	"validationErrors.systemDir": "Sistem dizini kullanılamaz",
	"validationErrors.notWritable": "Konum yazılabilir değil",
	"validationErrors.notFound": "Dizin mevcut değil",
	"validationErrors.homeRoot": "Ana dizin kökü kullanılamaz",
	freeSpaceLabel: "{{label}} ({{freeSpace}} GB boş)"
};
var gameGrid$5 = {
	loading: "Oyunlar yükleniyor...",
	noGamesFound: "Oyun bulunamadı",
	noGamesHint: "Kütüphanelerinizi Unifideck ayarlarından senkronize etmeyi deneyin",
	showingGames: "{{count}} oyun gösteriliyor",
	installed: "Yüklü",
	notInstalled: "Yüklü Değil",
	playtime: "{{hours}} sa {{minutes}} dk oynandı"
};
var downloadsTab$5 = {
	currentDownload: "AKTİF İNDİRME",
	queuedDownloads: "SIRADA ({{count}})",
	recentlyCompleted: "SON TAMAMLANANLAR",
	noActiveDownloads: "Aktif indirme yok",
	noDownloads: "İndirme yok. Kütüphanenizdeki oyunları burada görmek için yükleyin.",
	loadingLabel: "Yükleniyor...",
	loadingMessage: "İndirme sırası alınıyor...",
	confirmCancelTitle: "İptali Onayla",
	confirmCancelDescription: "{{game}} indirmesini iptal etmek istediğinizden emin misiniz?",
	confirmCancelYes: "Evet",
	confirmCancelNo: "Hayır",
	cancelDownload: "İptal Et",
	toastDownloadCancelledTitle: "İndirme İptal Edildi",
	toastDownloadCancelledBody: "İndirme sıradan kaldırıldı.",
	toastCancelFailedTitle: "İptal Başarısız",
	toastCancelFailedBody: "İndirme iptal edilemedi: {{error}}",
	status: {
		queued: "Sırada",
		downloading: "İndiriliyor",
		completed: "Tamamlandı",
		cancelled: "İptal Edildi",
		error: "Hata"
	},
	preparingDownload: "İndirme hazırlanıyor...",
	phaseExtracting: "{{game}} ayıklanıyor...",
	phaseVerifying: "{{game}} doğrulanıyor...",
	internalStorage: "Dahili Depolama ({{freeSpace}} GB boş)",
	sdCard: "SD Kart ({{freeSpace}} GB boş)",
	customLocation: "Özel: {{path}} ({{freeSpace}} GB boş)",
	extractingLabel: "Ayıklanıyor...",
	verifyingLabel: "Doğrulanıyor...",
	updateQueuedLabel: "Güncelleme sırada",
	downloadQueuedLabel: "İndirme sırada",
	downloadingUpdateLabel: "Güncelleme indiriliyor",
	downloadingLabel: "İndiriliyor...",
	finalizingInstallation: "Kurulum tamamlanıyor...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "Kalan Süre: {{eta}}",
	preparingMessage: "Ön ek hazırlanıyor ve bulut kayıtları eşitleniyor...",
	preparingLabel: "Oyun hazırlanıyor...",
	installingViaUpcLabel: "Ubisoft Connect ile yükleniyor",
	extractingMessage: "Dosyalar çıkarılıyor...",
	verifyingMessage: "Dosyalar doğrulanıyor... {{pct}}%",
	installingViaUpcMessage: "Kurulumu tamamlamak için Ubisoft Connect penceresini izleyin..."
};
var cloudSave$5 = {
	title: "Bulut Kaydı Çakışması",
	description: "{{game}} için yerel verileriniz bulut kayıtlarından farklı",
	local: "Yerel:",
	cloud: "Bulut:",
	newer: "(daha yeni)",
	useCloud: "Bulut Kaydını Kullan",
	useLocal: "Yerel Kaydı Kullan",
	unknownDate: "Bilinmeyen",
	toastUsingCloudTitle: "Bulut Kaydı Kullanılıyor",
	toastUsingCloudBody: "{{game}} için bulut kayıtları indiriliyor",
	toastUsingLocalTitle: "Yerel Kayıt Kullanılıyor",
	toastUsingLocalBody: "{{game}} için yerel kayıtlar karşıya yükleniyor",
	toastErrorTitle: "Hata",
	toastErrorBody: "Çakışma çözülemedi",
	filesSize: "{{count}} dosya · {{size}}",
	uploadBlockedEmpty: "{{game}} için buluta yükleme atlandı — yerel kayıt bulunamadı",
	uploadSkippedTitle: "Buluta yükleme atlandı",
	uploadSkippedBody: "{{game}} için henüz yüklenecek yerel kayıt yok."
};
var storeConnections$5 = {
	title: "MAĞAZA BAĞLANTILARI",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "Bağlı",
	checking: "Kontrol ediliyor...",
	authenticate: "Giriş Yap",
	legendaryNotInstalled: "⚠️ Epic Games aracı yükleniyor...",
	nileNotInstalled: "⚠️ Amazon Games aracı bulunamadı",
	error: "Hata — Günlükleri kontrol edin"
};
var librarySync$5 = {
	title: "KÜTÜPHANE SENK.",
	syncLibraries: "Kütüphaneleri Senkronize Et",
	forceSync: "Zorla Senkronize Et",
	syncing: "Senkronize ediliyor...",
	cancelSync: "Senkronizasyonu İptal Et",
	retryStatusCheck: "Durum Kontrolünü Yeniden Dene",
	gamesSynced: "{{synced}} / {{total}} oyun senkronize edildi",
	artworkDownloaded: "{{synced}} / {{total}} kapak görseli indirildi",
	metadataExtracted: "{{synced}} / {{total}} meta veri kaydı ayıklandı",
	steamMetadataDownloaded: "Steam Meta Verisi: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDB Meta Verisi: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacritic Verisi: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWG Meta Verisi: {{synced}} / {{total}}",
	compatChecked: "Uyumluluk Verisi: {{synced}} / {{total}}",
	cancelling: "İptal ediliyor…",
	error: "Hata"
};
var cleanup$5 = {
	title: "Temizlik",
	deleteAll: "Tüm UNIFIDECK kütüphanelerini ve önbelleğini sil",
	modalTitle: "Tüm Unifideck Verileri Silinsin mi?",
	modalDescription: "Bu işlem tüm Unifideck kısayollarını, Steam kapak görsellerini, kayıtlı mağaza girişlerini ve önbelleğe alınmış verileri kaldıracaktır. Yüklü oyunlar diskte kalacak ve yeniden senkronize edilebilecektir. Yüklü oyun dosyalarını da silmek için aşağıdaki yıkıcı modu etkinleştirin.",
	deleteFilesLabel: "Yüklü oyun dosyalarını da sil (Yıkıcı)",
	deleteFilesDescription: "Güvenli bir kök dizin altındaki her Unifideck oyunu için diskteki yükleme dizinini siler.",
	confirmDelete: "Evet, Hepsini Sil",
	cancel: "İptal Et",
	deleting: "Siliniyor..."
};
var captureLogs$5 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (tr-TR).",
	title: "Günlükleri topla",
	description: "Tüm Unifideck günlüklerini İndirilenler klasörünüzdeki tek bir zip dosyasında toplar, böylece bir hata bildirimine ekleyebilirsiniz.",
	capture: "Günlükleri topla",
	capturing: "Günlükler toplanıyor...",
	savedTo: "İndirilenler klasörünüze kaydedildi",
	bundleInfo: "{{files}} dosya · {{size}}",
	partialNote: "{{count}} dosya okunamadı. Paket yine de kullanılabilir.",
	partialNote_other: "{{count}} dosya okunamadı. Paket yine de kullanılabilir.",
	copyPath: "Yolu kopyala",
	modalTitle: "Tanılama paketi oluşturulsun mu?",
	modalDescription: "Unifideck günlüklerini İndirilenler klasörünüzdeki tek bir zip dosyasında toplayacak, böylece bir hata bildirimine ekleyebilirsiniz.",
	modalIncludes: "İçerir: eklenti ve oyun başlatma günlükleri, eşitleme ve kurulum geçmişi, oyun adlarınız ve kurulum yolları, ayrıca model, işletim sistemi ve depolama gibi cihaz bilgileri.",
	modalExcludes: "Asla içermez: şifreleriniz, mağaza oturum belirteçleriniz veya tarayıcı çerezleriniz.",
	modalConfirm: "Paket oluştur",
	cancel: "İptal"
};
var installButton$5 = {
	play: "Oyna",
	resume: "Devam Et",
	install: "{{title}} Yükle",
	installNative: "Yükle",
	uninstall: "{{title}} Kaldır",
	cancel: "✖ İptal Et",
	processing: "İşleniyor...",
	checking: "Kontrol ediliyor...",
	size: "{{size}} GB",
	update: "GÜNCELLE",
	updateAvailable: "GÜNCELLEME MEVCUT",
	updateAvailableDescription: "Yeni bir sürüm yüklenmeye hazır."
};
var confirmModals$5 = {
	installTitle: "Yüklemeyi Onayla",
	installDescription: "{{title}} oyununu yüklemek istediğinizden emin misiniz?",
	uninstallTitle: "Kaldırmayı Onayla",
	uninstallDescription: "{{title}} oyununu kaldırmak istediğinizden emin misiniz?",
	cancelTitle: "İptali Onayla",
	cancelDescription: "{{title}} indirmesini iptal etmek istediğinizden emin misiniz?",
	forceSyncTitle: "Zorla Senkronizasyon Ayarları",
	forceSyncDescription: "Tüm kapak görsellerini yeniden senkronize etmek istiyor musunuz? Bu işlem, görsellerde yaptığınız tüm manuel değişikliklerin üzerine yazacaktır. Sadece eksik görselleri indirmek için 'Görselleri Koru' seçeneğini seçin.",
	resyncArtwork: "Görselleri Yeniden Senkronize Et",
	keepArtwork: "Görselleri Koru",
	steamRestartTitle: "Senkronizasyon Tamamlandı!",
	steamRestartDescription: "Yeni oyunlarınızı ve kapak görsellerinizi uygulamak için lütfen Steam'i yeniden başlatın",
	steamRestartDescriptionStore: "Yeni {{store}} oyunlarınızı ve kapak görsellerinizi uygulamak için lütfen Steam'i yeniden başlatın",
	steamRestartCleanupTitle: "Temizlik Tamamlandı!",
	steamRestartCleanupDescription: "Silinen oyunları ve kapak görsellerini tamamen kaldırmak için lütfen Steam'i yeniden başlatın",
	restartNow: "Şimdi Yeniden Başlat",
	later: "Daha Sonra",
	ubisoftInstallTitle: "Ubisoft Connect Gerekli",
	ubisoftInstallDescription: "Bu işlem, {{title}} oyununu yüklemek için ihtiyaç duyacağınız Ubisoft Connect başlatıcısını yükleyecektir.",
	ubisoftInstallConfirm: "Ubisoft Connect'i Aç",
	yes: "Evet",
	no: "Hayır",
	amazonUpdateTitle: "Tam İndirme Gerekli",
	amazonUpdateDescription: "Amazon güncellemeleri {{title}} oyununun tamamen yeniden indirilmesini gerektirir. Bu işlem biraz zaman alabilir. Devam edilsin mi?",
	updateConfirm: "Güncelle"
};
var toasts$5 = {
	downloadStarted: "İndirme Başlatıldı. Şurayı kontrol edin: UNIFIDECK > İndirmeler",
	downloadQueued: "İndirme sırada",
	downloadQueuedBody: "{{title}} sıraya eklendi.",
	downloadFailedUnknown: "Bilinmeyen hata.",
	updateQueued: "Güncelleme sırada",
	uninstallDone: "Kaldırma işlemi tamamlandı.",
	ubisoftLauncherOpening: "Ubisoft Connect Açılıyor",
	ubisoftLauncherOpeningMessage: "{{title}} için Ubisoft Connect açılıyor. Oyunu orada yükleyin ve ardından oynamak için geri dönün.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect bu kurulum için zaten açık.",
	ubisoftLauncherOpenFailed: "Ubisoft Connect Açılamadı",
	ubisoftLauncherOpenFailedMessage: "Kurulum için Ubisoft Connect açılamadı.",
	multipartDetected: "Çok parçalı indirme algılandı",
	multipartMessage: "Lütfen sabırlı olun ve tamamlanmasını bekleyin",
	downloadFailed: "İndirme Başarısız",
	downloadFailedMessage: "İndirme başlatılamadı.",
	downloadCancelled: "İndirme İptal Edildi",
	downloadCancelledMessage: "{{title}} indirmesi iptal edildi.",
	cancelFailed: "İptal Başarısız",
	cancelFailedMessage: "İndirme iptal edilemedi.",
	uninstalling: "Kaldırılıyor...",
	uninstallingMessage: "{{title}} kaldırılıyor...",
	uninstallingMessageProton: "{{title}} ve Proton dosyaları kaldırılıyor...",
	uninstallComplete: "Kaldırma Tamamlandı",
	uninstallCompleteMessage: "{{title}} kaldırıldı.",
	uninstallCompleteMessageProton: "{{title}} kaldırıldı (Proton dosyaları dahil).",
	installComplete: "Yükleme Tamamlandı!",
	installCompleteMessage: "{{title}} oynamaya hazır.",
	syncComplete: "Senkronizasyon Tamamlandı!",
	syncCompleteMessage: "{{count}} oyun eklendi. Kütüphanenizde görmek için STEAM'İ YENİDEN BAŞLATIN (sadece oyun moduna dönmeyin, Steam'den tamamen çıkıp yeniden girin).",
	forceSyncComplete: "Zorla Senkronizasyon Tamamlandı!",
	forceSyncCompleteMessage: "{{count}} oyun güncellendi. Değişiklikleri görmek için STEAM'İ YENİDEN BAŞLATIN.",
	syncCancelled: "Senkronizasyon İptal Edildi",
	authTimeout: "Kimlik Doğrulama Zaman Aşımı",
	authTimeoutMessage: "{{store}} kimlik doğrulaması zaman aşımına uğradı. Lütfen tekrar deneyin.",
	authFailed: "Kimlik Doğrulama Başarısız",
	authFailedMessage: "Kimlik doğrulaması başlatılamadı",
	authError: "Kimlik Doğrulama Hatası",
	authConnected: "{{store}} Bağlandı!",
	authConnectedMessage: "{{store}} ile başarıyla kimlik doğrulaması yapıldı",
	cleanupStarted: "Temizlik Başlatıldı",
	cleanupStartedMessage: "Kısayollar, kapak görselleri ve kayıtlı giriş bilgileri kaldırılıyor. Bu biraz zaman alabilir…",
	cleanupSuccessful: "Temizlik Başarıyla Tamamlandı",
	cleanupSuccessfulMessage: "{{games}} kısayol, {{artwork}} kapak görseli kaldırıldı, {{stores}} mağazadan çıkış yapıldı, {{files}} yükleme dizini silindi. Önbellek temizlendi.",
	cleanupNoop: "Temizlenecek Bir Şey Yok",
	cleanupNoopMessage: "Unifideck'te kaldırılacak kısayol, kapak görseli veya kayıtlı giriş bilgisi bulunmuyor.",
	deleteFailed: "Silme Başarısız",
	repairPrefixSuccess: "Önek Onarıldı",
	repairPrefixSuccessMessage: "Ubisoft Connect öneki yeniden oluşturuldu",
	repairPrefixFailed: "Onarım Başarısız",
	repairPrefixFailedMessage: "Önek onarılamadı: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "Ağ Hatası",
		launchRetry: "Başlatma yeniden deneniyor",
		retryingUmu: "{{seconds}} saniye içinde UMU yeniden denenecek (deneme {{attempt}}/{{max}})...",
		protonUpgrade: "Proton Güncellemesi",
		resettingPrefix: "{{version}} için önek sıfırlanıyor...",
		firstTimeSetup: "İlk Kurulum",
		initializingPrefix: "Wine öneki hazırlanıyor...",
		setupCompleteTitle: "Kurulum Tamamlandı",
		prefixInitialized: "Wine öneki hazırlandı",
		setupFallback: "Kurulum",
		fallbackInitialization: "Geri dönüş hazırlığı...",
		launchingGame: "Oyun Başlatılıyor",
		startingEpicGame: "Epic oyunu başlatılıyor...",
		startingAmazonGame: "Amazon oyunu başlatılıyor...",
		startingGogGame: "GOG oyunu başlatılıyor...",
		startingUbisoftGame: "Ubisoft oyunu başlatılıyor...",
		dependenciesTitle: "Bağımlılıklar Yükleniyor",
		installingRedistributables: "Oyun gereksinimleri yükleniyor...",
		dependenciesReady: "Bağımlılıklar Hazır",
		redistributablesInstalled: "Gereksinimler yüklendi",
		dependenciesStatus: "Bağımlılıklar",
		checkLogs: "Kurulum tamamlandı (detaylar için günlüklere bakın)",
		installingProton: "Proton Yükleniyor",
		attemptingInstall: "{{version}} yüklenmeye çalışılıyor...",
		epicAuthRequired: "Epic Girişi Gerekli",
		epicAuthRequiredMessage: "Lütfen Unifideck ayarlarından Epic Games'e giriş yapın",
		installingRedist: "Gereksinimler Yükleniyor",
		installingRedistMessage: "GOG gereksinimlerinin ilk kurulumu yapılıyor...",
		setupFailed: "Kurulum Başarısız",
		setupFailedMessage: "Kurulum başarısız oldu, detaylar için günlüklere bakın",
		prerequisitesTitle: "Ön Koşullar Yükleniyor",
		installingPrerequisites: "Gerekli oyun ön koşulları yükleniyor...",
		prerequisitesReady: "Ön Koşullar Hazır",
		prerequisitesInstalled: "Oyun ön koşulları yüklendi",
		prerequisitesFailed: "Ön Koşul Uyarısı",
		prerequisitesFailedMessage: "Ön koşullar düzgün yüklenememiş olabilir",
		browserRequired: "Microsoft Edge Gerekli",
		signingIn: "Giriş yapılıyor...",
		cloudSyncDownFailed: "{{store}} için bulut kaydı indirilemedi ({{error}}). Yerel kayıt ile oynanıyor.",
		cloudSyncUpFailed: "{{store}} için bulut kaydı karşıya yüklenemedi ({{error}}). İlerlemeniz yalnızca yerel olarak kaydedildi.",
		errorCircuitBreakerOpen: "{{game_key}} son zamanlarda {{count}} kez başlatılamadı. Otomatik başlatmalar askıya alındı.",
		launcherError: "{{game_key}} başlatılamadı ({{error_code}}). Detaylar için günlüklere bakın.",
		downloadingProton: "Proton indiriliyor — bu birkaç dakika sürebilir...",
		downloadingRuntime: "Steam Runtime indiriliyor — ilk başlatma birkaç dakika sürebilir...",
		installingUbisoft: "Ubisoft Connect ile yükleniyor",
		installingUbisoftMessage: "Ubisoft Connect açılıyor — kurulumu başlatıcı penceresinden başlatın.",
		protonReadyBody: "{{version}} kullanıma hazır",
		protonReadyTitle: "Proton hazır",
		protonSwitchedTo: "Şimdi {{version}} kullanılıyor",
		signingInUbisoft: "Ubisoft Connect'te oturum açılıyor",
		signingInUbisoftMessage: "Ubisoft Connect açılıyor — orada oturum açın, sonra geri dönün.",
		ubisoftPrefixNotReady: "Ubisoft Connect hazır değil",
		ubisoftPrefixNotReadyMessage: "Bu oyun için Ubisoft Connect ön eki hazırlanamadı. Tekrar yüklemeyi deneyin; sorun devam ederse kitaplık eşitlemesini yeniden çalıştırın.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect hazır değil",
		ubisoftAuthPrefixNotReadyMessage: "Ubisoft Connect oturum açma için hazırlanamadı. Lütfen birazdan tekrar deneyin.",
		ubisoftLaunchIdMissing: "Ubisoft Connect açılıyor",
		ubisoftLaunchIdMissingMessage: "Bu oyunun başlatma kimliği çözümlenemedi — kitaplığınızdan başlatabilmeniz için Ubisoft Connect açılıyor. Doğrudan başlatmayı düzeltmek için kitaplık eşitlemesini yeniden çalıştırın."
	},
	updateComplete: "Güncelleme Tamamlandı!",
	updateCompleteMessage: "{{title}} güncellendi.",
	updateCancelled: "Güncelleme İptal Edildi",
	updateCancelledMessage: "{{title}} güncellemesi iptal edildi.",
	updateFailed: "Güncelleme Başarısız",
	updateFailedMessage: "Güncelleme başlatılamadı.",
	storeError: "{{store}} ile iletişim kurulamadı ({{errType}})",
	captureLogsStarted: "Günlükler toplanıyor...",
	captureLogsStartedMessage: "İndirilenler klasörünüzde bir tanılama paketi oluşturuluyor. Bu birkaç saniye sürer.",
	captureLogsDone: "Günlükler toplandı",
	captureLogsDoneMessage: "{{path}} konumuna kaydedildi ({{size}}, {{files}} dosya).",
	captureLogsPartial: "Günlükler toplandı (kısmi)",
	captureLogsPartialMessage: "{{path}} konumuna kaydedildi ({{size}}, {{files}} dosya). {{failures}} dosya okunamadı.",
	captureLogsFailed: "Günlükler toplanamadı",
	captureLogsBusy: "Zaten bir günlük toplama işlemi sürüyor",
	captureLogsCopied: "Yol kopyalandı",
	captureLogsCopyFailed: "Yol kopyalanamadı",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "Yer Aç",
		signInToStore: "Giriş Yap",
		retrySync: "Yeniden Dene",
		retryLibrarySync: "Kütüphaneyi Yenile",
		openSaveFolder: "Kayıt Klasörünü Göster",
		showLogs: "Günlükleri Göster"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "{{store}} kütüphanesi yenilenemedi ({{error}}). Mevcut kütüphaneniz görünmeye devam edecek."
	},
	cloudPullStarted: "Bulut kaydı indiriliyor…",
	cloudPushStarted: "Yerel kaydı yükleniyor…",
	cloudSavePathFailed: "Kayıt konumu ayarlanamadı",
	cloudSavePathSet: "Kayıt konumu güncellendi",
	cloudPullDone: "Bulut kaydı indirildi",
	cloudPullFailed: "Bulut kaydı indirilemedi",
	cloudPushDone: "Yerel kayıt yüklendi",
	cloudPushFailed: "Yerel kayıt yüklenemedi"
};
var uninstallModal$5 = {
	title: "Kaldırmayı Onayla",
	description: "{{title}} oyununu kaldırmak istediğinizden emin misiniz?",
	deleteProtonLabel: "Proton Dosyalarını Da Sil",
	deleteProtonDescription: "Uyumluluk verilerini ve kullanıcı dosyalarını kaldırır",
	warningTitle: "Uyarı:",
	warningBody: "Bu işlem, Wine öneğinde bulunan kayıt verilerini kalıcı olarak silecektir. Yedeğiniz olduğundan emin olun.",
	cancel: "İptal Et",
	uninstall: "Kaldır"
};
var languageSettings$5 = {
	title: "DİL",
	label: "Görüntüleme Dili",
	description: "Arayüz dili ve oyun indirme dili (destekleniyorsa)",
	autoDetect: "Otomatik (Sistem)"
};
var errors$5 = {
	legendaryNotFound: "Epic Games CLI (Legendary) bulunamadı",
	gogdlNotFound: "GOG CLI (gogdl) bulunamadı",
	nileNotFound: "Amazon Games CLI (Nile) bulunamadı",
	authUrlFailed: "Kimlik doğrulama URL'si alınamadı",
	authFailed: "Kimlik doğrulama başarısız",
	notAuthenticated: "Kimliğiniz doğrulanmadı. Lütfen önce giriş yapın.",
	syncInProgress: "Zaten bir senkronizasyon devam ediyor",
	syncCancelled: "Senkronizasyon kullanıcı tarafından iptal edildi",
	gameNotFound: "Oyun bulunamadı",
	gameNotInstalled: "Oyun yüklü değil",
	installFailed: "Yükleme başarısız oldu — detaylar için günlüklere bakın",
	installNoDirectory: "Yükleme tamamlandı ancak oyun klasörü bulunamadı",
	uninstallFailed: "Kaldırma başarısız",
	lockConflict: "Kilit çakışması — lütfen tekrar deneyin",
	unknownStore: "Bilinmeyen mağaza",
	shortcutWriteFailed: "Kısayollar yazılamadı",
	backgroundSyncDisabled: "Arka plan senkronizasyonu devre dışı bırakıldı",
	steamGridDbUnavailable: "SteamGridDB kütüphanesi kullanılamıyor",
	deleteSyncInProgress: "Senkronizasyon devam ederken silme işlemi yapılamaz",
	alreadyInQueue: "Zaten sırada",
	download: {
		diskSpace: "Yetersiz disk alanı",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "Ağ hatası — bağlantınızı kontrol edin",
		authExpired: "Oturum süresi doldu — lütfen tekrar giriş yapın",
		toolMissing: "Gerekli indirme aracı bulunamadı",
		processFailed: "İndirme başarısız — lütfen tekrar deneyin",
		directoryNotFound: "Yüklü dosyalar bulunamadı",
		lockConflict: "Başka bir işlem devam ediyor — tekrar deneyin",
		ubisoftClient: "Ubisoft Connect istemci hatası — öneki onarmayı deneyin",
		ubisoftClientNotRunning: "Ubisoft Connect istemcisi yanıt vermiyor",
		ubisoftBootstrap: "Ubisoft Connect öneki yapılandırılamadı",
		ubisoftUmuMissing: "umu-run bulunamadı — Ubisoft oyunları için gereklidir",
		generic: "Yükleme başarısız oldu — detaylar için günlüklere bakın"
	},
	unknown: "Bilinmeyen hata",
	steamAppsUnavailable: "SteamClient.Apps API'si kullanılamıyor"
};
var sync$5 = {
	start: "Kütüphaneleri Senkronize Et",
	force: "Zorla Senkronize Et",
	syncing: "Senkronize ediliyor…",
	cancel: "Senkronizasyonu İptal Et",
	cancelling: "İptal ediliyor…",
	preparing: "Hazırlanıyor…",
	downloadingArtwork: "{{game}} için kapak görseli indiriliyor...",
	fetchingGameLists: "Oyun listeleri alınıyor...",
	fetchingStore: "{{store}} alınıyor ({{current}}/{{total}})...",
	cancelledByUser: "Senkronizasyon kullanıcı tarafından iptal edildi",
	checkingInstalledGames: "Yüklü oyunlar kontrol ediliyor...",
	sgdbLookup: "{{count}} oyun SteamGridDB'de aranıyor...",
	extractingSteamMetadata: "Steam meta verileri ayıklanıyor...",
	fetchingEnhancedMetadata: "Gelişmiş meta veriler alınıyor...",
	lookingUpUnifiDB: "unifiDB meta verileri alınıyor...",
	fetchingMetacriticData: "Metacritic verileri alınıyor...",
	fetchingCompatData: "Uyumluluk kontrol ediliyor...",
	checkingExistingArtwork: "Mevcut görseller kontrol ediliyor...",
	cleaningOrphanedArtwork: "Artık görseller temizleniyor...",
	retryingMissingArtwork: "Görseli eksik {{synced}} / {{total}} oyun için tekrar deneniyor...",
	savingShortcuts: "Kısayollar kaydediliyor...",
	completed: "Senkronizasyon Tamamlandı!",
	extractingMetadata: "{{game}} için meta veriler ayıklanıyor...",
	cooldown: "{{secs}} saniye bekleyin",
	artworkPhase: "Görsel {{done}} / {{total}}"
};
var force_sync$5 = {
	migratingOldInstallations: "Zorla Senkronizasyon: Eski kurulumlar taşınıyor...",
	fetchingGameLists: "Zorla Senkronizasyon: Oyun listeleri alınıyor...",
	checkingInstalledGames: "Zorla Senkronizasyon: Yüklü oyunlar kontrol ediliyor...",
	rewritingShortcuts: "Zorla Senkronizasyon: Kısayollar yeniden yazılıyor...",
	cancelledShortcutsSaved: "Zorla Senkronizasyon iptal edildi - kısayollar kaydedildi",
	writingShortcuts: "Zorla Senkronizasyon: Kısayollar yazılıyor...",
	clearingProton: "Zorla Senkronizasyon: Proton uyumluluğu temizleniyor...",
	completed: "Zorla Senkronizasyon Tamamlandı! {{updated}} oyun güncellendi, {{artwork}} görsel alındı."
};
var artwork$5 = {
	checking: "Görseller kontrol ediliyor...",
	queueRefresh: "Görsel güncellemesi için tüm oyunlar sıraya alınıyor...",
	downloadProgress: "İndirilen görseller: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$5 = {
	greatOnDeck: "Deck'te Harika",
	allGames: "Tüm Oyunlar",
	installed: "Yüklü",
	nonSteam: "Steam Dışı",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$5 = {
	error: "Unifideck Hatası",
	errorLoadingView: "Birleşik kütüphane görünümü yüklenemedi",
	checkConsole: "Detaylar için tarayıcı konsolunu kontrol edin",
	errorLoadingGames: "Oyunlar yüklenirken hata oluştu",
	tryReloading: "Eklentiyi yeniden yüklemeyi deneyin veya konsolu kontrol edin",
	storeLabel: "Mağaza:",
	allStores: "Tüm Mağazalar",
	searchPlaceholder: "Oyun ara...",
	library: "Kütüphane"
};
var gameInfoPanel$5 = {
	loading: "Oyun bilgileri yükleniyor...",
	noMetadata: "Bu oyun için meta veri bulunmuyor.",
	noTestResults: "Detaylı test sonucu bulunmuyor.",
	buttons: {
		details: "Detaylar",
		synopsis: "Özet",
		storePage: "Mağaza Sayfası",
		dlc: "DLC",
		communityHub: "Topluluk Merkezi",
		pointsShop: "Puan Dükkanı",
		discussions: "Tartışmalar",
		guides: "Rehberler",
		support: "Destek",
		install: "Yükle",
		uninstall: "Kaldır",
		cancel: "İptal Et",
		achievements: "Başarımlar"
	},
	labels: {
		store: "Mağaza:",
		name: "İsim:",
		size: "Boyut:",
		developer: "Geliştirici:",
		publisher: "Yayıncı:",
		released: "Çıkış Tarihi:",
		metacritic: "Metacritic:",
		status: "Durum:",
		spaceRequired: "Gerekli Alan",
		lastPlayed: "Son Oynama",
		playTime: "Oynama Süresi",
		never: "Hiçbir Zaman"
	},
	compatibility: {
		modalTitle: "Steam Deck Uyumluluğu",
		viewOnProtonDb: "ProtonDB'de Görüntüle",
		close: "Kapat",
		verified: "ONAYLANDI",
		playable: "OYNANABİLİR",
		unsupported: "DESTEKLENMİYOR",
		unknown: "BİLİNMİYOR"
	},
	failedToLoadMetadata: "Meta veriler yüklenemedi",
	lastSession: "Son oturum: {{names}} açıldı"
};
var gameDetailsSettings$5 = {
	title: "OYUN DETAYLARI GÖRÜNÜMÜ",
	simple: "Basit",
	detailed: "Detaylı"
};
var gogLanguageModal$5 = {
	title: "Dil Seçin",
	description: "{{title}} için hangi dilin indirileceğini seçin",
	label: "Dil",
	install: "Yükle",
	cancel: "İptal Et"
};
var accountSwitch$5 = {
	title: "Hesap Değişikliği Algılandı",
	description: "Farklı bir Steam hesabına giriş yaptınız. Unifideck verileriyle ne yapmak istersiniz?",
	migrate: "Unifideck Verilerini Taşı",
	migrateDescription: "Önceki hesaptan oyunları ve kapak resimlerini kopyala",
	freshStart: "Temiz Başlangıç",
	freshStartDescription: "Diğer Epic/GOG/Amazon/Ubisoft hesaplarını bağlamak için kayıtlı mağaza girişlerini temizle",
	skip: "Geç",
	toastMigrateTitle: "Taşıma Tamamlandı",
	toastMigrateBody: "{{shortcuts}} kısayol ve {{artwork}} kapak resmi dosyası taşındı",
	toastClearTitle: "Kimlik Doğrulama Belirteçleri Temizlendi",
	toastClearBody: "Mağaza giriş bilgileri temizlendi. Artık farklı hesaplarla giriş yapabilirsiniz."
};
var ubisoftAuth$5 = {
	title: "Ubisoft Connect Girişi",
	description: "Kütüphanenizi bağlamak için Ubisoft Connect aracılığıyla giriş yapın.",
	signInViaUpc: "Ubisoft Connect ile Giriş Yap",
	upcDescription: "Ubisoft Connect giriş için açılacak. Giriş yaptıktan sonra oturumunuz otomatik olarak yakalanacaktır.",
	launchingUpc: "Ubisoft Connect başlatılıyor...",
	waitingForLogin: "Giriş yapılması bekleniyor...",
	retryLaunch: "Tekrar Dene",
	cancel: "İptal",
	launchFailed: "Ubisoft Connect başlatılamadı. Lütfen tekrar deneyin.",
	signingIn: "Giriş yapılıyor...",
	emailLabel: "E-posta",
	passwordLabel: "Şifre",
	signIn: "Giriş Yap",
	twoFactorTitle: "İki Faktörlü Doğrulama",
	twoFactorDescription: "Kimlik doğrulama uygulamanızdaki doğrulama kodunu girin.",
	twoFactorLabel: "Doğrulama Kodu",
	verify: "Doğrula",
	verifying: "Doğrulanıyor...",
	errorEmptyFields: "Lütfen e-posta ve şifrenizi girin",
	errorEmpty2FA: "Lütfen doğrulama kodunu girin",
	errorLoginFailed: "Giriş başarısız. Lütfen bilgilerinizi kontrol edin.",
	errorInvalid2FA: "Geçersiz doğrulama kodu. Lütfen tekrar deneyin.",
	showPassword: "Şifreyi Göster",
	hidePassword: "Şifreyi Gizle",
	doubleLoginWarning: "Ubisoft'a iki kez giriş yapmanız gerekecektir."
};
var relativeDate$5 = {
	today: "Bugün",
	yesterday: "Dün"
};
var common$5 = {
	thisGame: "bu oyunu",
	game: "Oyun",
	unknown: "Bilinmiyor",
	ok: "Tamam",
	cancel: "İptal",
	working: "İşleniyor…",
	loading: "Yükleniyor…",
	free: "ücretsiz",
	close: "Kapat"
};
var info$5 = {
	expandDetails: "Ayrıntıları Genişlet",
	collapseDetails: "Ayrıntıları Gizle"
};
var microsoft$5 = {
	notCompatible: "Bu oyun formatı SteamOS ile uyumlu değil",
	playOnCloud: "Bulutta Oyna",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Microsoft / Xbox hesabınızla giriş yapın",
	accountConnected: "Microsoft Hesabı Bağlandı",
	loggedOut: "Microsoft Store oturumu kapatıldı",
	xcloudNotInstalled: "xCloud oyunları yerel olarak yüklenmez",
	chromiumRequired: "Microsoft Edge Gerekli",
	chromiumRequiredMessage: "Giriş yapmak için Microsoft Edge tarayıcısı gereklidir. Otomatik olarak yüklemek ister misiniz? (Bu işlem yalnızca bir kez gereklidir).",
	chromiumInstallButton: "Microsoft Edge Yükle",
	chromiumInstalling: "Microsoft Edge yükleniyor...",
	chromiumInstalled: "Microsoft Edge yüklendi!",
	chromiumAlreadyInstalled: "Uyumlu tarayıcı zaten yüklü",
	chromiumInstallFailed: "Microsoft Edge yüklenemedi. Lütfen manuel olarak yükleyin: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Microsoft Edge yükleme zaman aşımına uğradı. İnternet bağlantınızı kontrol edin.",
	browserAlreadyInstalled: "Uyumlu tarayıcı zaten yüklü",
	browserInstalled: "Microsoft Edge başarıyla yüklendi!",
	browserInstallFailed: "Microsoft Edge yüklenemedi. Lütfen manuel olarak yükleyin: flatpak install com.microsoft.Edge",
	flatpakNotFound: "Flatpak bu sistemde mevcut değil",
	signInRequired: "Oynamak için Giriş Yapın",
	syncSkippedNoSubscription: "Aktif bir Xbox Game Pass aboneliği bulunamadı. xCloud kütüphaneniz senkronize edilmedi.",
	syncSkippedTierUnknown: "Xbox Game Pass aboneliği algılandı ancak abonelik düzeyi belirlenemedi. Kullanılamayan oyunların gösterilmesini önlemek için senkronizasyon atlandı.",
	subscriptionCheckFailed: "Xbox Game Pass aboneliği doğrulanamadı. xCloud senkronizasyonu atlandı. Bağlantınız kararlı olduğunda tekrar deneyin.",
	subscriptionDetected: "Aktif Xbox Game Pass {{tier}} aboneliği algılandı."
};
var playButton$5 = {
	play: "Oyna",
	install: "Yükle",
	cancel: "İptal Et",
	retry: "Yeniden Dene",
	downloadFailed: "İndirme Başarısız",
	installing: "Yükleniyor",
	controllerConfig: "Kontrolcü Yapılandırması",
	appSettings: "Uygulama Ayarları"
};
var play$5 = {
	play: "Oyna",
	resume: "Devam Et",
	stop: "Durdur",
	cancel: "İptal Et",
	cancelling: "İptal ediliyor…",
	update: "Güncelle",
	uninstall: "Kaldır",
	queued: "Kuyrukta",
	downloading: "İndiriliyor",
	extracting: "Çıkartılıyor",
	verifying: "Doğrulanıyor",
	finalizing: "Yükleme tamamlanıyor",
	cancelConfirmTitle: "İndirmeyi iptal et?",
	cancelConfirmBody: "{{title}} indirmesini iptal etmek istediğinizden emin misiniz? Kısmen indirilen dosyalar silinecektir.",
	cancelConfirmConfirm: "İndirmeyi İptal Et",
	cancelConfirmCancel: "İndirmeye Devam Et",
	playOnCloud: "Bulutta Oyna",
	cloudSave: {
		autoPullOn: "Bulut kayıtları başlangıçta otomatik olarak indirilir.",
		close: "Kapat",
		download: "Bulut kaydını indir",
		filesCount: "{{count}} dosya",
		label: "Bulut kayıtları",
		menuTitle: "Bulut kayıtları — {{game}}",
		noCloudData: "Bilinmeyen",
		noCloudSupport: "Bulut kaydı desteği yok",
		setLocation: "Kayıt konumunu ayarla",
		setLocationHint: "Bu oyunun kayıtlarını sakladığı klasörü seçin.",
		setLocationTitle: "Bu oyunun kayıt klasörünü seçin",
		statusCloudAvailable: "Bulut kaydı kullanılabilir",
		statusNoSaves: "Yerel kayıt yok",
		statusSyncing: "Bulut kayıtları eşitleniyor…",
		statusUnresolved: "Kayıt konumu bulunamadı — eşitleme bu oyun için çalışmayabilir.",
		unresolvedHint: "Kayıt klasörünü tespit edebilmemiz için oyunu bir kez başlatın veya konumu manuel olarak ayarlayın.",
		upload: "Yerel kaydı yükle"
	},
	exe: {
		browse: "Göz at…",
		changeFailed: "Çalıştırılabilir dosya değiştirilemedi",
		changed: "Çalıştırılabilir dosya değiştirildi",
		defaultTag: "(varsayılan)",
		menuItem: "Çalıştırılabilir dosyayı değiştir…",
		reset: "Varsayılan çalıştırılabilir dosyaya sıfırla",
		resetButton: "Varsayılana sıfırla",
		subtitle: "Bu oyunun hangi dosyayı başlatacağını seçin (örn. bir başlatıcıyı atlayıp oyunu doğrudan çalıştırmak için). Çalışma dizini ve bulut kayıtları etkilenmez.",
		title: "Çalıştırılabilir dosyayı değiştir — {{game}}",
		unavailable: "Bu oyunun klasörü okunamadı."
	}
};
var playMeta$5 = {
	spaceRequired: "Gerekli Alan",
	installedSize: "Yüklü Boyut",
	lastPlayed: "Son Oynama",
	neverPlayed: "Hiç Oynanmadı",
	hoursShort: "sa",
	minutesShort: "dk",
	played: "Oynandı"
};
var downloads$5 = {
	title: "İndirmeler",
	empty: "Aktif indirme yok. Kütüphanenizden oyun yükleyerek burada görebilirsiniz.",
	current: "Şu Anda İndirilen",
	queued: "Kuyrukta",
	finished: "Son Tamamlananlar",
	cancel: "İptal Et",
	removeFromQueue: "Kaldır",
	play: "Oyna",
	outcome: {
		queued: "Kuyrukta",
		downloading: "İndiriliyor",
		completed: "Tamamlandı",
		cancelled: "İptal Edildi",
		error: "Başarısız"
	}
};
var navigation$5 = {
	loading: "Yükleniyor...",
	storesTitle: "Mağazalar",
	syncTitle: "Kütüphane Senk.",
	languageTitle: "Dil"
};
var security$5 = {
	panelTitle: "Güvenlik",
	counters: {
		label: "Sayaçlar",
		empty: "Başlangıçtan bu yana olay gözlemlenmedi"
	},
	log: {
		label: "Denetim Günlüğü",
		entryCount: "{{count}} kayıt",
		entryCount_other: "{{count}} kayıt",
		emptyFiltered: "Filtreyle eşleşen olay yok"
	},
	filter: {
		label: "Filtre",
		all: "Tüm Olaylar"
	},
	timeline: {
		label: "Son 24 Saat ({{count}} olay)",
		label_other: "Son 24 Saat ({{count}} olay)",
		axisStart: "-24sa",
		axisEnd: "şimdi"
	},
	relativeTime: {
		seconds: "{{count}} sn önce",
		minutes: "{{count}} dk önce",
		hours: "{{count}} sa önce",
		days: "{{count}} gün önce"
	},
	bruteforce: {
		warningTitle: "Uyarı: Kaba kuvvet girişimi algılandı (uyarı seviyesi)",
		escalationTitle: "ALARM: Kaba kuvvet girişimi algılandı (yükseltilmiş seviye)",
		detail: "Son {{window}} saniye içinde {{failures}} başarısız şifre çözme denemesi yapıldı. Eşikler: {{warning}} (uyarı) / {{escalation}} (yükseltilmiş).",
		resetButton: "Doğrula ve Dedektörü Sıfırla"
	},
	actions: {
		clearLog: "Günlüğü ve Sayaçları Temizle"
	},
	events: {
		tokenEncrypted: "Belirteç şifrelendi",
		tokenDecrypted: "Belirteç çözüldü",
		decryptFailed: "Şifre çözme başarısız",
		tokenFileMigrated: "Belirteç dosyası taşındı",
		legacyPlaintextDetected: "Eski düz metin algılandı",
		authFlowStarted: "Kimlik doğrulama akışı başlatıldı",
		authFlowCompleted: "Kimlik doğrulama akışı tamamlandı",
		authFlowFailed: "Kimlik doğrulama akışı başarısız",
		permissionsCheck: "İzin denetimi yapıldı",
		permissionsRepaired: "İzinler düzeltildi",
		bruteforceSuspected: "Kaba kuvvet şüphesi",
		deviceResetDetected: "Cihaz sıfırlaması algılandı",
		fingerprintInitialized: "Parmak izi başlatıldı",
		externalAuthCheckFailed: "Harici kimlik doğrulama denetimi başarısız",
		configValidationCompleted: "Yapılandırma doğrulaması tamamlandı",
		configValidationFailed: "Yapılandırma doğrulaması başarısız"
	},
	configValidation: {
		degradedTitle: "Geçersiz Yapılandırma: Kısıtlı mod aktif",
		detail: "Başlangıçta {{count}} doğrulama hatası algılandı",
		detail_other: "Başlangıçta {{count}} doğrulama hatası algılandı",
		sourceDefaults: "varsayılanlar",
		sourceUser: "kullanıcı ayarları",
		sourceMerged: "birleştirilmiş",
		overflow: "+ {{count}} diğer hata",
		overflow_other: "+ {{count}} diğer hata"
	}
};
var cloudSyncBehavior$5 = {
	_comment: "Kullanıcının her mağaza için bulut kayıt senkronizasyon hatalarının nasıl gösterileceğini seçmesine olanak tanıyan ayarlar paneli. Beş adet store_label girişi CloudSyncBehaviorPanel.tsx tarafından işlenen satırları yansıtır. Microsoft kasıtlı olarak hariç tutulmuştur: xCloud akışının senkronize edilecek yerel bir kaydı yoktur.",
	title: "Bulut Senkronizasyon Davranışı",
	description: "Oyun başlatılırken bir bulut kayıt indirmesi veya yüklemesi başarısız olduğunda Unifideck'in nasıl tepki vereceğini seçin.",
	loadError: "Mevcut ayarlar yüklenemedi",
	saveError: "Değişiklik kaydedilemedi. Lütfen tekrar deneyin",
	store_label: {
		"default": "Varsayılan (Tüm Mağazalar)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "Sessiz (Yalnızca günlüğe kaydet)",
		toast: "Bildir (Açılır pencere bildirimi)"
	}
};
var cloudSync$5 = {
	_comment: "Bulut senkronizasyonu başarısız olduğunda başlatıcı tarafından oluşturulan açılır pencereler. Hata, arka plandan gelen yükteki error_i18n_key alanı kullanılarak cloudSync.error.<code> üzerinden çözümlenir.",
	error: {
		network_unreachable: "Ağa erişilemiyor",
		auth_expired: "Oturum süresi doldu",
		forbidden: "Erişim yasaklandı",
		quota_exceeded: "Bulut depolama kotası aşıldı",
		server_error: "Mağaza sunucusuna erişilemiyor",
		timed_out: "İstek zaman aşımına uğradı",
		disk_full: "Disk dolu",
		disk_space_low: "Atlandı: Disk alanı yetersiz (alan boşaltın ve tekrar deneyin)",
		permission_denied: "İzin reddedildi",
		cancelled: "İptal edildi",
		unknown: "Beklenmeyen hata"
	},
	unavailableNativeDep: "Bu sistemde bulut kayıtları kapalı — Python ({{python}}) sürümünde gerekli bir bileşen eksik. Yerel kayıtlarınız bu durumdan etkilenmez; eşitlemeyi geri yüklemek için sistem güncellemesinden sonra eklentiyi yeniden derleyin/yükleyin."
};
var launchLogs$5 = {
	_comment: "LaunchLogsModal için mesajlar. Mod penceresi, başlatıcı hata uyarılarına (devre kesici + BaşlatıcıHatası) eklenmiş olan unifideck://show-logs/<launch_id> bildirim eyleminden açılır. ~/.local/share/unifideck/launches/<launch_id>.log adresindeki her başlatma için arşivlenmiş günlükleri okur ve önem derecesine göre renklendirilmiş olarak görüntüler.",
	title: "Başlatma Günlükleri — {{launchId}}",
	close: "Kapat",
	copyAll: "Tümünü Kopyala",
	"export": "Dosyaya Aktar",
	copied: "Panoya kopyalandı",
	copyFailed: "Kopyalama başarısız",
	exporting: "Aktarılıyor…",
	exportedTo: "{{path}} konumuna aktarıldı",
	exportFailed: "Aktarma hatası: {{error}}",
	loading: "Günlükler yükleniyor…",
	notAvailable: "Bu başlatmanın günlükleri artık mevcut değil (7 günlük saklama süresinin dışında).",
	error: "Günlükler okunamadı: {{error}}",
	fileInfo: "{{path}} — {{total}} satırdan {{shown}} satır gösteriliyor",
	filterLabel: "Göster",
	filterInfo: "Bilgi",
	filterWarning: "Uyarı",
	filterError: "Hata",
	nextError: "Sonraki Hata ({{current}}/{{total}})",
	noErrors: "Hata yok",
	allFiltered: "Tüm satırlar geçerli filtre tarafından gizlendi. Girişleri görmek için en az bir önem düzeyini etkinleştirin."
};
var settings$5 = {
	_comment: "Daha özel bileşenler (LanguageSelector, CloudSyncBehaviorPanel) tarafından işlenmeyen UnifideckSettingsPanel eylem kontrolleri. refreshAll, kayıtlı tüm mağazalarda sync_all() işlevini tetikleyen bir düğmedir; kütüphane görünümü, kendi SYNC_PROGRESS olay aboneliği aracılığıyla senkronizasyon ilerlemesini görüntüler.",
	storeConnections: "Mağaza Bağlantıları",
	librarySync: "Kütüphane Senk.",
	language: "Dil",
	languageAuto: "Otomatik Algıla",
	refreshAll: {
		sectionTitle: "Kütüphane Eylemleri",
		button: "Tüm Kütüphaneleri Yenile",
		buttonBusy: "Başlatılıyor…",
		startedTitle: "Kütüphane Yenileme Başlatıldı",
		startedBody: "Tüm mağazalar arka planda senkronize ediliyor.",
		startedSingle: "{{store}} kütüphanesi arka planda yenileniyor.",
		failedTitle: "Kütüphane yenileme başlatılamadı"
	}
};
var saveFolder$5 = {
	_comment: "SaveFolderModal için mesajlar. Mod penceresi, genellikle bulut senkronizasyonu hatalarında kullanıcının yerel olarak neyin depolandığını incelemek istemesi durumunda, unifideck://open-save-folder/<store>/<game_id> bildirim eyleminden açılır. count ve size alanları interpolasyonludur; truncated yalnızca liste kısıtlandığında gösterilir.",
	title: "Kayıt Klasörü — {{store}} / {{gameId}}",
	close: "Kapat",
	copyPath: "Yolu Kopyala",
	copyFileList: "Dosya Listesini Kopyala",
	depthLabel: "Derinlik",
	filterLabel: "Filtre",
	loading: "Klasör içeriği yükleniyor…",
	noSavesYet: "Bu oyun için henüz yerel kayıt bulunamadı. Klasör bir sonraki başarılı bulut senkronizasyonunda oluşturulacaktır.",
	aggregate: "{{count}} dosya, toplam {{size}}",
	truncated: "Diğer {{count}} dosya ({{size}}) gösterilmedi. Bunları görmek için filtreyi daraltın.",
	error: "Kayıt klasörü okunamadı: {{error}}"
};
var library$5 = {
	_comment: "Başlatma devre kesici göstergesi ve tekrarlayan hatalarda başlatmaya zorlama / sıfırlama kontrolleri dahil olmak üzere kütüphane görünümündeki oyun başına kullanıcı arabirimi hizmetleri.",
	circuitBreaker: {
		_comment: "Başlatma hatası devre kesicisi arayüzü. Gösterge yalnızca yapılandırılan zaman penceresindeki hata sayısı circuit_breaker.failures_threshold (varsayılan olarak 600 saniyede 3) değerine ulaştığında görünür. Başlatmaya zorlama korumayı bir kez atlar; Sıfırlama tüm hata geçmişini temizler.",
		badgeRecent: "{{count}} son başlatma hatası",
		badgeRecent_other: "{{count}} son başlatma hatası",
		badgeTooltip: "Son zaman penceresinde {{count}} başarısız başlatma. Devre kesici otomatik başlatmayı askıya aldı.",
		forceLaunchButton: "Başlatmaya Zorla",
		resetButton: "Hataları Sıfırla",
		failureKind: {
			fastBoot: "Hızlı önyükleme çökmeleri",
			launcherError: "Başlatıcı hazırlık hatası"
		}
	}
};
var pickStorage$5 = {
	title: "Yükleme Konumu",
	customOption: "Farklı bir konum seçin...",
	customLabel: "Özel Konum",
	change: "Değiştir...",
	gameSize: "Yükleme Boyutu"
};
var restart$5 = {
	title: "Senkronizasyon Tamamlandı!",
	body: "Yeni oyunlarınızı ve kapak görsellerinizi uygulamak için lütfen Steam'i yeniden başlatın",
	confirm: "Şimdi Yeniden Başlat",
	later: "Daha Sonra"
};
var updater$5 = {
	checkButton: "Güncellemeleri Denetle",
	checkCompleteMessage: "En son sürüm bilgisi başarıyla alındı.",
	checkCompleteTitle: "Güncelleme Denetimi Tamamlandı",
	checkFailedTitle: "Denetim Başarısız",
	checkingButton: "Denetleniyor...",
	currentTitle: "Mevcut",
	downgradeButton: "v{{version}} sürümüne düşür",
	installFailedTitle: "Yükleme Başarısız",
	installTimeoutMessage: "Decky Loader'dan yanıt yok — yükleme takılmış veya başarısız olmuş olabilir. Lütfen tekrar deneyin.",
	installedLabel: "yüklendi",
	installingButton: "Yükleniyor...",
	installingTitle: "Eklenti Yükleniyor",
	latestLabel: "en son",
	modalTitle: "UNIFIDECK v{{version}} — Sürüm Notları",
	noBackend: "Decky arka ucu kullanılamıyor.",
	noReleaseNotes: "Sürüm notu bulunmuyor.",
	reinstallButton: "v{{version}} sürümünü yeniden yükle",
	refreshFailedMessage: "En son sürüm bilgisi doğrulanamadı. Bağlantınızı kontrol edip tekrar deneyin.",
	releaseGoneMessage: "Bu sürüm artık kullanılamıyor. Lütfen güncellemeleri denetleyip yeniden seçin.",
	releaseNotesButton: "Sürüm Notları",
	stageDownload: "İndiriliyor…",
	stageFinish: "Tamamlanıyor…",
	stageInstall: "Yükleniyor…",
	stageOpen: "Paket okunuyor…",
	stageParse: "Doğrulanıyor…",
	stageRemove: "Eski sürüm kaldırılıyor…",
	stageStart: "Başlatılıyor…",
	stageWorking: "Yükleniyor…",
	titleLoading: "Sürüm Denetleniyor",
	typeDowngrade: "Sürüm düşürülüyor:",
	typeReinstall: "Yeniden yükleniyor",
	typeUpdate: "Güncelleniyor:",
	updateButton: "v{{version}} sürümüne güncelle"
};
var achievements$5 = {
	unlockedTitle: "Başarım kilidi açıldı",
	unlockedBody: "{{name}}"
};
var achievementsModal$5 = {
	title: "Başarımlar",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "Yenile",
	close: "Kapat",
	loading: "Başarımlar yükleniyor…",
	empty: "Bu oyunda başarım yok.",
	hidden: "Gizli başarım",
	unlockedOn: "{{date}} tarihinde açıldı",
	error: "Başarımlar yüklenemedi.",
	errorOffline: "Başarımları görüntülemek için internete bağlanın.",
	errorAuth: "Başarımları görüntülemek için bu oyunun mağazasında oturum açın.",
	errorUnavailable: "Bu oyun için başarımlar mevcut değil."
};
var collectionSettings$5 = {
	title: "STEAM KOLEKSİYONLARI",
	enabled: "Koleksiyonlar etkin",
	disabled: "Koleksiyonlar devre dışı"
};
var trTR = {
	_comment: _comment$5,
	tabs: tabs$5,
	auth: auth$5,
	storage: storage$5,
	storageSettings: storageSettings$5,
	gameGrid: gameGrid$5,
	downloadsTab: downloadsTab$5,
	cloudSave: cloudSave$5,
	storeConnections: storeConnections$5,
	librarySync: librarySync$5,
	cleanup: cleanup$5,
	captureLogs: captureLogs$5,
	installButton: installButton$5,
	confirmModals: confirmModals$5,
	toasts: toasts$5,
	uninstallModal: uninstallModal$5,
	languageSettings: languageSettings$5,
	errors: errors$5,
	sync: sync$5,
	force_sync: force_sync$5,
	artwork: artwork$5,
	deckTabs: deckTabs$5,
	unifiedLibrary: unifiedLibrary$5,
	gameInfoPanel: gameInfoPanel$5,
	gameDetailsSettings: gameDetailsSettings$5,
	gogLanguageModal: gogLanguageModal$5,
	accountSwitch: accountSwitch$5,
	ubisoftAuth: ubisoftAuth$5,
	relativeDate: relativeDate$5,
	common: common$5,
	info: info$5,
	microsoft: microsoft$5,
	playButton: playButton$5,
	play: play$5,
	playMeta: playMeta$5,
	downloads: downloads$5,
	navigation: navigation$5,
	security: security$5,
	cloudSyncBehavior: cloudSyncBehavior$5,
	cloudSync: cloudSync$5,
	launchLogs: launchLogs$5,
	settings: settings$5,
	saveFolder: saveFolder$5,
	library: library$5,
	pickStorage: pickStorage$5,
	restart: restart$5,
	updater: updater$5,
	achievements: achievements$5,
	achievementsModal: achievementsModal$5,
	collectionSettings: collectionSettings$5
};

var _comment$4 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$4 = {
	settings: "設定",
	downloads: "ダウンロード"
};
var auth$4 = {
	connect: "連携する",
	disconnect: "解除する",
	status: {
		connected: "連携済み",
		disconnected: "未連携",
		checking: "確認中…",
		error: "エラー — ログを確認してください"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$4 = {
	title: "ストレージ",
	defaultUpdated: "既定のストレージを更新しました",
	isDefault: "既定",
	setDefault: "既定に設定",
	customPath: "カスタムパス",
	up: "↑ 上へ",
	refresh: "更新",
	useThisPath: "このパスを使用する"
};
var storageSettings$4 = {
	title: "ダウンロード設定",
	installLocationLabel: "インストール先",
	installLocationDescription: "新しく追加するゲームのダウンロード先",
	pathLabel: "パス",
	loading: "ストレージの選択肢を読み込み中...",
	toastUpdatedTitle: "ストレージの場所を更新しました",
	toastUpdatedBody: "新しいゲームは {{location}} にインストールされます",
	toastFailedTitle: "更新に失敗しました",
	toastFailedBody: "{{error}}",
	unknown: "不明",
	browseButton: "参照...",
	clearCustom: "カスタムの場所をクリア",
	customPathSet: "カスタムのインストール先を設定しました",
	newFolder: "新しいフォルダ",
	newFolderCreate: "作成",
	selectFolder: "このフォルダを使用",
	emptyDirectory: "空のフォルダ",
	showHidden: "非表示ファイルを表示",
	selectDevice: "デバイスを選択",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "更新日時 (新しい順)",
	sortModifiedOldest: "更新日時 (古い順)",
	sortCreatedNewest: "作成日時 (新しい順)",
	sortCreatedOldest: "作成日時 (古い順)",
	sortSizeLargest: "サイズ (大きい順)",
	sortSizeSmallest: "サイズ (小さい順)",
	"validationErrors.notAbsolute": "パスは絶対パスで指定してください",
	"validationErrors.systemDir": "システムディレクトリは指定できません",
	"validationErrors.notWritable": "指定した場所への書き込み権限がありません",
	"validationErrors.notFound": "ディレクトリが存在しません",
	"validationErrors.homeRoot": "ホームのルートディレクトリは指定できません",
	freeSpaceLabel: "{{label}} (空き容量 {{freeSpace}} GB)"
};
var gameGrid$4 = {
	loading: "ゲームを読み込み中...",
	noGamesFound: "ゲームが見つかりません",
	noGamesHint: "Unifideckの設定からライブラリの同期を試してください",
	showingGames: "{{count}} 個のゲームを表示中",
	installed: "インストール済み",
	notInstalled: "未インストール",
	playtime: "プレイ時間: {{hours}} 時間 {{minutes}} 分"
};
var downloadsTab$4 = {
	currentDownload: "現在のダウンロード",
	queuedDownloads: "待機中 ({{count}})",
	recentlyCompleted: "最近完了したダウンロード",
	noActiveDownloads: "アクティブなダウンロードはありません",
	noDownloads: "ダウンロードはありません。ライブラリからゲームをインストールすると、ここに表示されます。",
	loadingLabel: "読み込み中...",
	loadingMessage: "ダウンロードキューを取得中...",
	confirmCancelTitle: "キャンセルの確認",
	confirmCancelDescription: "{{game}} のダウンロードをキャンセルしてもよろしいですか？",
	confirmCancelYes: "はい",
	confirmCancelNo: "いいえ",
	cancelDownload: "キャンセル",
	toastDownloadCancelledTitle: "ダウンロードをキャンセルしました",
	toastDownloadCancelledBody: "ダウンロードはキューから削除されました。",
	toastCancelFailedTitle: "キャンセルに失敗しました",
	toastCancelFailedBody: "ダウンロードをキャンセルできませんでした: {{error}}",
	status: {
		queued: "待機中",
		downloading: "ダウンロード中",
		completed: "完了",
		cancelled: "キャンセル済み",
		error: "エラー"
	},
	preparingDownload: "ダウンロードを準備中...",
	phaseExtracting: "{{game}} を展開中...",
	phaseVerifying: "{{game}} を検証中...",
	internalStorage: "内蔵ストレージ (空き容量 {{freeSpace}} GB)",
	sdCard: "SDカード (空き容量 {{freeSpace}} GB)",
	customLocation: "カスタム: {{path}} (空き容量 {{freeSpace}} GB)",
	extractingLabel: "展開中...",
	verifyingLabel: "検証中...",
	updateQueuedLabel: "アップデート待機中",
	downloadQueuedLabel: "ダウンロード待機中",
	downloadingUpdateLabel: "アップデートをダウンロード中",
	downloadingLabel: "ダウンロード中...",
	finalizingInstallation: "インストールを完了しています...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "残り時間: {{eta}}",
	preparingMessage: "プレフィックスを初期化し、クラウドセーブを同期しています...",
	preparingLabel: "ゲームを準備しています...",
	installingViaUpcLabel: "Ubisoft Connect でインストール中",
	extractingMessage: "ファイルを展開しています...",
	verifyingMessage: "ファイルを検証しています... {{pct}}%",
	installingViaUpcMessage: "Ubisoft Connect のウィンドウに従ってインストールを完了してください..."
};
var cloudSave$4 = {
	title: "クラウドセーブの競合",
	description: "{{game}} のローカルデータがクラウドセーブと異なります",
	local: "ローカル:",
	cloud: "クラウド:",
	newer: "(新しいほう)",
	useCloud: "クラウドを使用する",
	useLocal: "ローカルを使用する",
	unknownDate: "不明",
	toastUsingCloudTitle: "クラウドセーブを使用中",
	toastUsingCloudBody: "{{game}} のクラウドセーブをダウンロードしています",
	toastUsingLocalTitle: "ローカルセーブを使用中",
	toastUsingLocalBody: "{{game}} のローカルセーブをクラウドにアップロードしています",
	toastErrorTitle: "エラー",
	toastErrorBody: "競合を解決できませんでした",
	filesSize: "{{count}} 個のファイル · {{size}}",
	uploadBlockedEmpty: "{{game}} のクラウドアップロードをスキップしました — ローカルのセーブが見つかりません",
	uploadSkippedTitle: "クラウドアップロードをスキップ",
	uploadSkippedBody: "{{game}} にアップロードするローカルセーブはまだありません。"
};
var storeConnections$4 = {
	title: "ストア連携",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "連携済み",
	checking: "確認中...",
	authenticate: "ログインする",
	legendaryNotInstalled: "⚠️ Epic Games ツールをインストール中...",
	nileNotInstalled: "⚠️ Amazon Games ツールが見つかりません",
	error: "エラー — ログを確認してください"
};
var librarySync$4 = {
	title: "ライブラリ同期",
	syncLibraries: "ライブラリを同期する",
	forceSync: "強制同期する",
	syncing: "同期中...",
	cancelSync: "同期をキャンセル",
	retryStatusCheck: "ステータス確認を再試行",
	gamesSynced: "{{synced}} / {{total}} 本のゲームを同期しました",
	artworkDownloaded: "{{synced}} / {{total}} 枚のアートワークをダウンロードしました",
	metadataExtracted: "{{synced}} / {{total}} 件のメタデータを抽出しました",
	steamMetadataDownloaded: "Steamメタデータ: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDBメタデータ: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacriticデータ: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWGメタデータ: {{synced}} / {{total}}",
	compatChecked: "互換性データ: {{synced}} / {{total}}",
	cancelling: "キャンセル中…",
	error: "エラー"
};
var cleanup$4 = {
	title: "クリーンアップ",
	deleteAll: "UNIFIDECK のすべてのライブラリとキャッシュを削除",
	modalTitle: "Unifideck のすべてのデータを削除しますか？",
	modalDescription: "これにより、すべての Unifideck ショートカット、Steam アートワーク、保存されたストアのログイン情報、およびキャッシュデータが削除されます。インストールされたゲームはディスク上に残り、再同期が可能です。インストールされたゲームのファイルも削除するには、以下の破壊的モードを有効にしてください。",
	deleteFilesLabel: "インストールされたゲームのファイルも削除する (破壊的)",
	deleteFilesDescription: "安全なルートディレクトリ配下にある各 Unifideck ゲームのディスク上のインストールディレクトリを削除します。",
	confirmDelete: "はい、すべて削除します",
	cancel: "キャンセル",
	deleting: "削除中..."
};
var captureLogs$4 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (ja-JP).",
	title: "ログを収集",
	description: "Unifideck のすべてのログを 1 つの ZIP ファイルにまとめてダウンロードフォルダーに保存します。バグ報告に添付できます。",
	capture: "ログを収集",
	capturing: "ログを収集中...",
	savedTo: "ダウンロードフォルダーに保存しました",
	bundleInfo: "{{files}} 個のファイル · {{size}}",
	partialNote: "{{count}} 個のファイルを読み取れませんでした。このパッケージは引き続き利用できます。",
	partialNote_other: "{{count}} 個のファイルを読み取れませんでした。このパッケージは引き続き利用できます。",
	copyPath: "パスをコピー",
	modalTitle: "診断パッケージを作成しますか？",
	modalDescription: "Unifideck はログを 1 つの ZIP ファイルにまとめてダウンロードフォルダーに保存します。バグ報告に添付できます。",
	modalIncludes: "含まれるもの: プラグインとゲーム起動のログ、同期とインストールの履歴、ゲームのタイトルとインストール先のパス、モデル・OS・ストレージなどのデバイス情報。",
	modalExcludes: "含まれないもの: パスワード、ストアのログイントークン、ブラウザーの Cookie。",
	modalConfirm: "パッケージを作成",
	cancel: "キャンセル"
};
var installButton$4 = {
	play: "プレイ",
	resume: "再開",
	install: "{{title}} をインストール",
	installNative: "インストール",
	uninstall: "{{title}} をアンインストール",
	cancel: "✖ キャンセル",
	processing: "処理中...",
	checking: "確認中...",
	size: "{{size}} GB",
	update: "アップデート",
	updateAvailable: "アップデートがあります",
	updateAvailableDescription: "新しいバージョンがインストール可能です。"
};
var confirmModals$4 = {
	installTitle: "インストールの確認",
	installDescription: "{{title}} をインストールしてもよろしいですか？",
	uninstallTitle: "アンインストールの確認",
	uninstallDescription: "{{title}} をアンインストールしてもよろしいですか？",
	cancelTitle: "キャンセルの確認",
	cancelDescription: "{{title}} のダウンロードをキャンセルしてもよろしいですか？",
	forceSyncTitle: "強制同期設定",
	forceSyncDescription: "すべてのアートワークを再同期しますか？これにより、アートワークに加えた手動の変更が上書きされます。不足しているアートワークのみをダウンロードするには、「アートワークを保持」を選択してください。",
	resyncArtwork: "アートワークを再同期",
	keepArtwork: "アートワークを保持",
	steamRestartTitle: "同期が完了しました！",
	steamRestartDescription: "新しいゲームとアートワークを反映するには、Steam を再起動してください",
	steamRestartDescriptionStore: "新しいゲームと {{store}} のアートワークを反映するには、Steam を再起動してください",
	steamRestartCleanupTitle: "クリーンアップが完了しました！",
	steamRestartCleanupDescription: "削除されたゲームとアートワークを完全に反映するには、Steam を再起動してください",
	restartNow: "今すぐ再起動",
	later: "後で",
	ubisoftInstallTitle: "Ubisoft Connect が必要です",
	ubisoftInstallDescription: "これにより、{{title}} をインストールするために必要な Ubisoft Connect ランチャーがロードされます。",
	ubisoftInstallConfirm: "Ubisoft Connect を開く",
	yes: "はい",
	no: "いいえ",
	amazonUpdateTitle: "完全な再ダウンロードが必要です",
	amazonUpdateDescription: "Amazon のアップデートには、{{title}} の完全な再ダウンロードが必要です。これには時間がかかる場合があります。続行しますか？",
	updateConfirm: "アップデート"
};
var toasts$4 = {
	downloadStarted: "ダウンロードを開始しました。UNIFIDECK > ダウンロード を確認してください",
	downloadQueued: "ダウンロードを待機中に追加しました",
	downloadQueuedBody: "{{title}} がキューに追加されました。",
	downloadFailedUnknown: "不明なエラーです。",
	updateQueued: "アップデートを待機中に追加しました",
	uninstallDone: "アンインストールが完了しました。",
	ubisoftLauncherOpening: "Ubisoft Connect を起動しています",
	ubisoftLauncherOpeningMessage: "{{title}} のために Ubisoft Connect を起動しています。そちらでゲームをインストールしたあと、ここに戻ってプレイしてください。",
	ubisoftLauncherAlreadyOpenMessage: "このインストールに対して Ubisoft Connect はすでに開いています。",
	ubisoftLauncherOpenFailed: "Ubisoft Connect を開けませんでした",
	ubisoftLauncherOpenFailedMessage: "インストールのための Ubisoft Connect の起動に失敗しました。",
	multipartDetected: "マルチパートダウンロードを検出しました",
	multipartMessage: "完了するまでお待ちください",
	downloadFailed: "ダウンロード失敗",
	downloadFailedMessage: "ダウンロードを開始できませんでした。",
	downloadCancelled: "ダウンロードキャンセル",
	downloadCancelledMessage: "{{title}} のダウンロードがキャンセルされました。",
	cancelFailed: "キャンセル失敗",
	cancelFailedMessage: "ダウンロードをキャンセルできませんでした。",
	uninstalling: "アンインストール中...",
	uninstallingMessage: "{{title}} をアンインストールしています...",
	uninstallingMessageProton: "{{title}} と Proton ファイルをアンインストールしています...",
	uninstallComplete: "アンインストール完了",
	uninstallCompleteMessage: "{{title}} を削除しました。",
	uninstallCompleteMessageProton: "{{title}} を削除しました (Proton ファイルを含みます)。",
	installComplete: "インストールが完了しました！",
	installCompleteMessage: "{{title}} はプレイ可能です。",
	syncComplete: "同期が完了しました！",
	syncCompleteMessage: "{{count}} 個のゲームを追加しました。ライブラリに反映させるため、STEAMを再起動してください (ゲームモードに戻るだけでなく、Steamを完全に再起動してください)。",
	forceSyncComplete: "強制同期が完了しました！",
	forceSyncCompleteMessage: "{{count}} 個のゲームを更新しました。変更を反映させるため、STEAMを再起動してください。",
	syncCancelled: "同期をキャンセルしました",
	authTimeout: "認証がタイムアウトしました",
	authTimeoutMessage: "{{store}} の認証期限が切れました。もう一度お試しください。",
	authFailed: "認証に失敗しました",
	authFailedMessage: "認証を開始できませんでした",
	authError: "認証エラー",
	authConnected: "{{store}} と連携しました！",
	authConnectedMessage: "{{store}} の認証に成功しました",
	cleanupStarted: "クリーンアップを開始しました",
	cleanupStartedMessage: "ショートカット、アートワーク、保存されたログイン情報を削除しています。これにはしばらく時間がかかる場合があります…",
	cleanupSuccessful: "クリーンアップが成功しました",
	cleanupSuccessfulMessage: "{{games}} 個のショートカットと {{artwork}} 枚のアートワークを削除し、{{stores}} 個のストアからログアウトし、{{files}} 個のインストール先ディレクトリを削除しました。キャッシュもクリアされました。",
	cleanupNoop: "クリーンアップするものはありません",
	cleanupNoopMessage: "削除するショートカット、アートワーク、保存されたログイン情報はありません。",
	deleteFailed: "削除に失敗しました",
	repairPrefixSuccess: "プレフィックスの修復完了",
	repairPrefixSuccessMessage: "Ubisoft Connect のプレフィックスを再構築しました",
	repairPrefixFailed: "修復に失敗しました",
	repairPrefixFailedMessage: "プレフィックスを修復できませんでした: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "ネットワークエラー",
		launchRetry: "起動を再試行しています",
		retryingUmu: "{{seconds}} 秒後に UMU を再試行します (試行 {{attempt}}/{{max}})...",
		protonUpgrade: "Proton アップグレード",
		resettingPrefix: "{{version}} のプレフィックスをリセット中...",
		firstTimeSetup: "初回セットアップ",
		initializingPrefix: "Wine プレフィックスを初期化中...",
		setupCompleteTitle: "セットアップ完了",
		prefixInitialized: "Wine プレフィックスが初期化されました",
		setupFallback: "セットアップ",
		fallbackInitialization: "フォールバック初期化中...",
		launchingGame: "ゲームを起動中",
		startingEpicGame: "Epic ゲームを起動中...",
		startingAmazonGame: "Amazon ゲームを起動中...",
		startingGogGame: "GOG ゲームを起動中...",
		startingUbisoftGame: "Ubisoft ゲームを起動中...",
		dependenciesTitle: "依存関係をインストール中",
		installingRedistributables: "ゲームに必要なランタイムをインストール中...",
		dependenciesReady: "依存関係準備完了",
		redistributablesInstalled: "ランタイムをインストールしました",
		dependenciesStatus: "依存関係",
		checkLogs: "セットアップ完了 (詳細はログを確認してください)",
		installingProton: "Proton をインストール中",
		attemptingInstall: "{{version}} のインストールを試行中...",
		epicAuthRequired: "Epic へのログインが必要です",
		epicAuthRequiredMessage: "Unifideck の設定から Epic Games にログインしてください",
		installingRedist: "ランタイムをインストール中",
		installingRedistMessage: "GOG ランタイムの初回インストールを実行しています...",
		setupFailed: "セットアップに失敗しました",
		setupFailedMessage: "インストールに失敗しました。詳細はログを確認してください",
		prerequisitesTitle: "必須コンポーネントをインストール中",
		installingPrerequisites: "必要なゲームコンポーネントをインストール中...",
		prerequisitesReady: "必須コンポーネント準備完了",
		prerequisitesInstalled: "必須ゲームコンポーネントがインストールされました",
		prerequisitesFailed: "コンポーネントに関する警告",
		prerequisitesFailedMessage: "コンポーネントが正しくインストールされなかった可能性があります",
		browserRequired: "Microsoft Edge が必要です",
		signingIn: "サインイン中...",
		cloudSyncDownFailed: "{{store}} のクラウドセーブのダウンロードに失敗しました ({{error}})。ローカルデータで起動します。",
		cloudSyncUpFailed: "{{store}} のクラウドセーブのアップロードに失敗しました ({{error}})。プレイ進行状況はローカルにのみ保存されます。",
		errorCircuitBreakerOpen: "{{game_key}} の起動に最近 {{count}} 回失敗しました。自動起動を一時停止します。",
		launcherError: "{{game_key}} を起動できませんでした ({{error_code}})。詳細はログを確認してください。",
		downloadingProton: "Proton をダウンロードしています — 数分かかる場合があります...",
		downloadingRuntime: "Steam Runtime をダウンロードしています — 初回起動には数分かかる場合があります...",
		installingUbisoft: "Ubisoft Connect でインストール中",
		installingUbisoftMessage: "Ubisoft Connect を開いています — ランチャーのウィンドウからインストールを開始してください。",
		protonReadyBody: "{{version}} を使用できます",
		protonReadyTitle: "Proton の準備完了",
		protonSwitchedTo: "現在 {{version}} を使用中",
		signingInUbisoft: "Ubisoft Connect にサインインしています",
		signingInUbisoftMessage: "Ubisoft Connect を開いています — そこでサインインしてから戻ってください。",
		ubisoftPrefixNotReady: "Ubisoft Connect の準備ができていません",
		ubisoftPrefixNotReadyMessage: "このゲームの Ubisoft Connect プレフィックスを準備できませんでした。もう一度インストールしてみてください。それでも失敗する場合は、ライブラリの同期を再実行してください。",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect の準備ができていません",
		ubisoftAuthPrefixNotReadyMessage: "サインイン用に Ubisoft Connect を準備できませんでした。しばらくしてからもう一度お試しください。",
		ubisoftLaunchIdMissing: "Ubisoft Connect を開いています",
		ubisoftLaunchIdMissingMessage: "このゲームの起動IDを解決できませんでした — ライブラリから起動できるように Ubisoft Connect を開いています。直接起動を修復するには、ライブラリの同期を再実行してください。"
	},
	updateComplete: "アップデートが完了しました！",
	updateCompleteMessage: "{{title}} をアップデートしました。",
	updateCancelled: "アップデートキャンセル",
	updateCancelledMessage: "{{title}} のアップデートがキャンセルされました。",
	updateFailed: "アップデート失敗",
	updateFailedMessage: "アップデートを開始できませんでした。",
	storeError: "{{store}} との通信に失敗しました ({{errType}})",
	captureLogsStarted: "ログを収集中...",
	captureLogsStartedMessage: "ダウンロードフォルダーに診断パッケージを作成しています。数秒かかります。",
	captureLogsDone: "ログを収集しました",
	captureLogsDoneMessage: "{{path}} に保存しました ({{size}}、{{files}} 個のファイル)。",
	captureLogsPartial: "ログを収集しました (一部)",
	captureLogsPartialMessage: "{{path}} に保存しました ({{size}}、{{files}} 個のファイル)。{{failures}} 個のファイルを読み取れませんでした。",
	captureLogsFailed: "ログを収集できませんでした",
	captureLogsBusy: "ログの収集は既に実行中です",
	captureLogsCopied: "パスをコピーしました",
	captureLogsCopyFailed: "パスをコピーできませんでした",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "空き容量を増やす",
		signInToStore: "ログインする",
		retrySync: "再試行",
		retryLibrarySync: "ライブラリを更新",
		openSaveFolder: "セーブフォルダを表示",
		showLogs: "ログを表示"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "{{store}} ライブラリの更新に失敗しました ({{error}})。既存のライブラリが引き続き表示されます。"
	},
	cloudPullStarted: "クラウドセーブをダウンロード中…",
	cloudPushStarted: "ローカルセーブをアップロード中…",
	cloudSavePathFailed: "セーブ場所を設定できませんでした",
	cloudSavePathSet: "セーブ場所が更新されました",
	cloudPullDone: "クラウドセーブがダウンロードされました",
	cloudPullFailed: "クラウドセーブのダウンロードに失敗しました",
	cloudPushDone: "ローカルセーブがアップロードされました",
	cloudPushFailed: "ローカルセーブのアップロードに失敗しました"
};
var uninstallModal$4 = {
	title: "アンインストールの確認",
	description: "{{title}} をアンインストールしてもよろしいですか？",
	deleteProtonLabel: "Proton ファイルも削除する",
	deleteProtonDescription: "互換性データとユーザーファイルを削除します",
	warningTitle: "警告:",
	warningBody: "これにより、Wine プレフィックス内にあるセーブデータが永久に削除されます。バックアップがあることを確認してください。",
	cancel: "キャンセル",
	uninstall: "アンインストール"
};
var languageSettings$4 = {
	title: "言語",
	label: "表示言語",
	description: "UIおよびゲームのダウンロード言語 (サポートされている場合)",
	autoDetect: "自動 (システム設定)"
};
var errors$4 = {
	legendaryNotFound: "Epic Games ツール (Legendary) が見つかりません",
	gogdlNotFound: "GOG ツール (gogdl) が見つかりません",
	nileNotFound: "Amazon Games ツール (Nile) が見つかりません",
	authUrlFailed: "認証 URL を取得できませんでした",
	authFailed: "認証に失敗しました",
	notAuthenticated: "認証されていません。先にログインしてください。",
	syncInProgress: "同期処理がすでに実行中です",
	syncCancelled: "ユーザーによって同期がキャンセルされました",
	gameNotFound: "ゲームが見つかりません",
	gameNotInstalled: "ゲームがインストールされていません",
	installFailed: "インストールに失敗しました — 詳細はログを確認してください",
	installNoDirectory: "インストールが完了しましたが、ゲームフォルダを特定できませんでした",
	uninstallFailed: "アンインストールに失敗しました",
	lockConflict: "ロックが競合しています — しばらくしてから再試行してください",
	unknownStore: "不明なストア",
	shortcutWriteFailed: "ショートカットの書き込みに失敗しました",
	backgroundSyncDisabled: "バックグラウンド同期が無効になっています",
	steamGridDbUnavailable: "SteamGridDB ライブラリが利用できません",
	deleteSyncInProgress: "同期処理中は削除できません",
	alreadyInQueue: "すでにキューに入っています",
	download: {
		diskSpace: "空き容量が不足しています",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "ネットワークエラー — 接続を確認してください",
		authExpired: "セッションが切れました — もう一度ログインしてください",
		toolMissing: "必要なダウンロードツールが見つかりません",
		processFailed: "ダウンロードに失敗しました — 再試行してください",
		directoryNotFound: "インストールされたファイルが見つかりません",
		lockConflict: "他の処理が実行中です — 再試行してください",
		ubisoftClient: "Ubisoft Connect エラー — プレフィックスの修復を試してください",
		ubisoftClientNotRunning: "Ubisoft Connect が応答していません",
		ubisoftBootstrap: "Ubisoft Connect プレフィックスを構成できませんでした",
		ubisoftUmuMissing: "umu-run が見つかりません — Ubisoft のゲームの起動に必要です",
		generic: "インストールに失敗しました — 詳細はログを確認してください"
	},
	unknown: "不明なエラー",
	steamAppsUnavailable: "SteamClient.Apps API が利用できません"
};
var sync$4 = {
	start: "ライブラリを同期する",
	force: "強制同期する",
	syncing: "同期中…",
	cancel: "同期をキャンセル",
	cancelling: "キャンセル中…",
	preparing: "準備中…",
	downloadingArtwork: "{{game}} のアートワークをダウンロード中...",
	fetchingGameLists: "ゲームリストを取得中...",
	fetchingStore: "{{store}} のゲームリストを取得中 ({{current}}/{{total}})...",
	cancelledByUser: "ユーザーによって同期がキャンセルされました",
	checkingInstalledGames: "インストール済みのゲームを確認中...",
	sgdbLookup: "SteamGridDB で {{count}} 個のゲームを検索中...",
	extractingSteamMetadata: "Steamメタデータを抽出中...",
	fetchingEnhancedMetadata: "拡張メタデータを取得中...",
	lookingUpUnifiDB: "unifiDBメタデータを取得中...",
	fetchingMetacriticData: "Metacriticメタデータを取得中...",
	fetchingCompatData: "互換性を確認中...",
	checkingExistingArtwork: "既存のアートワークを確認中...",
	cleaningOrphanedArtwork: "不要になったアートワークをクリーンアップ中...",
	retryingMissingArtwork: "アートワークのない {{synced}} / {{total}} 個のゲームを再試行中...",
	savingShortcuts: "ショートカットを保存中...",
	completed: "同期が完了しました！",
	extractingMetadata: "{{game}} のメタデータを抽出中...",
	cooldown: "{{secs}} 秒お待ちください",
	artworkPhase: "アートワーク {{done}} / {{total}}"
};
var force_sync$4 = {
	migratingOldInstallations: "強制同期: 古いインストールの移行中...",
	fetchingGameLists: "強制同期: ゲームリストを取得中...",
	checkingInstalledGames: "強制同期: インストール済みのゲームを確認中...",
	rewritingShortcuts: "強制同期: ショートカットの再書き込み中...",
	cancelledShortcutsSaved: "強制同期をキャンセルしました - ショートカットは保存されました",
	writingShortcuts: "強制同期: ショートカットの書き込み中...",
	clearingProton: "強制同期: Protonの互換性ファイルをクリア中...",
	completed: "強制同期が完了しました！ {{updated}} 個のゲームを更新し、 {{artwork}} 枚のアートワークを取得しました。"
};
var artwork$4 = {
	checking: "アートワークを確認中...",
	queueRefresh: "すべてのアートワークの更新をキューに追加中...",
	downloadProgress: "ダウンロードしたアートワーク: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$4 = {
	greatOnDeck: "Deckで快適",
	allGames: "すべてのゲーム",
	installed: "インストール済み",
	nonSteam: "非Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$4 = {
	error: "Unifideck エラー",
	errorLoadingView: "ライブラリ表示のロードに失敗しました",
	checkConsole: "詳細はブラウザのコンソールを確認してください",
	errorLoadingGames: "ゲームの読み込み中にエラーが発生しました",
	tryReloading: "プラグインの再ロードを試すか、コンソールを確認してください",
	storeLabel: "ストア:",
	allStores: "すべてのストア",
	searchPlaceholder: "ゲームを検索...",
	library: "ライブラリ"
};
var gameInfoPanel$4 = {
	loading: "ゲーム情報を読み込み中...",
	noMetadata: "このゲームのメタデータはありません。",
	noTestResults: "詳細なテスト結果はありません。",
	buttons: {
		details: "詳細",
		synopsis: "あらすじ",
		storePage: "ストアページ",
		dlc: "DLC",
		communityHub: "コミュニティハブ",
		pointsShop: "ポイントショップ",
		discussions: "スレッド",
		guides: "ガイド",
		support: "サポート",
		install: "インストール",
		uninstall: "アンインストール",
		cancel: "キャンセル",
		achievements: "実績"
	},
	labels: {
		store: "ストア:",
		name: "名前:",
		size: "サイズ:",
		developer: "開発元:",
		publisher: "パブリッシャー:",
		released: "リリース日:",
		metacritic: "Metacritic:",
		status: "ステータス:",
		spaceRequired: "必要な容量",
		lastPlayed: "最後にプレイ",
		playTime: "プレイ時間",
		never: "なし"
	},
	compatibility: {
		modalTitle: "Steam Deck 互換性",
		viewOnProtonDb: "ProtonDB で見る",
		close: "閉じる",
		verified: "確認済み",
		playable: "プレイ可能",
		unsupported: "非対応",
		unknown: "不明"
	},
	failedToLoadMetadata: "メタデータの読み込みに失敗しました",
	lastSession: "前回のセッション: {{names}} を解除"
};
var gameDetailsSettings$4 = {
	title: "ゲーム詳細表示",
	simple: "シンプル",
	detailed: "詳細表示"
};
var gogLanguageModal$4 = {
	title: "言語を選択",
	description: "{{title}} のダウンロード言語を選択してください",
	label: "言語",
	install: "インストール",
	cancel: "キャンセル"
};
var accountSwitch$4 = {
	title: "アカウントの変更を検出しました",
	description: "別のSteamアカウントでログインしています。Unifideckのデータをどう処理しますか？",
	migrate: "Unifideckデータの移行",
	migrateDescription: "前のアカウントからゲームとアートワークをコピーする",
	freshStart: "新規開始",
	freshStartDescription: "他のEpic/GOG/Amazon/Ubisoftアカウントを連携するために、保存されたログイン情報をクリアする",
	skip: "スキップ",
	toastMigrateTitle: "移行が完了しました",
	toastMigrateBody: "{{shortcuts}} 個のショートカットと {{artwork}} 枚のアートワークを移行しました",
	toastClearTitle: "認証トークンをクリアしました",
	toastClearBody: "ストアのログイン情報がリセットされました。別のアカウントでログインできます。"
};
var ubisoftAuth$4 = {
	title: "Ubisoft Connect ログイン",
	description: "ライブラリを連携するために、Ubisoft Connectでログインしてください。",
	signInViaUpc: "Ubisoft Connect経由でログイン",
	upcDescription: "Ubisoft Connectが起動しログイン画面が表示されます。ログイン成功後、セッションが自動的にインポートされます。",
	launchingUpc: "Ubisoft Connectを起動中...",
	waitingForLogin: "ログイン待機中...",
	retryLaunch: "再試行",
	cancel: "キャンセル",
	launchFailed: "Ubisoft Connectを起動できませんでした。もう一度お試しください。",
	signingIn: "サインイン中...",
	emailLabel: "メールアドレス",
	passwordLabel: "パスワード",
	signIn: "サインイン",
	twoFactorTitle: "2要素認証",
	twoFactorDescription: "認証アプリに表示される確認コードを入力してください。",
	twoFactorLabel: "確認コード",
	verify: "検証する",
	verifying: "検証中...",
	errorEmptyFields: "メールアドレスとパスワードを入力してください",
	errorEmpty2FA: "確認コードを入力してください",
	errorLoginFailed: "サインインに失敗しました。ログイン情報を確認してください。",
	errorInvalid2FA: "確認コードが無効です。もう一度お試しください。",
	showPassword: "パスワードを表示",
	hidePassword: "パスワードを非表示",
	doubleLoginWarning: "Ubisoftへのログインが2回必要になる場合があります。"
};
var relativeDate$4 = {
	today: "今日",
	yesterday: "昨日"
};
var common$4 = {
	thisGame: "このゲーム",
	game: "ゲーム",
	unknown: "不明",
	ok: "OK",
	cancel: "キャンセル",
	working: "処理中…",
	loading: "読み込み中…",
	free: "無料",
	close: "閉じる"
};
var info$4 = {
	expandDetails: "詳細を展開",
	collapseDetails: "詳細を折りたたむ"
};
var microsoft$4 = {
	notCompatible: "このゲーム形式は SteamOS と互換性がありません",
	playOnCloud: "クラウドでプレイ",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Microsoft / Xbox アカウントでログインしてください",
	accountConnected: "Microsoft アカウントと連携しました",
	loggedOut: "Microsoft Store からログアウトしました",
	xcloudNotInstalled: "xCloud のゲームはローカルにはインストールされません",
	chromiumRequired: "Microsoft Edge が必要です",
	chromiumRequiredMessage: "ログインするには Microsoft Edge ブラウザが必要です。自動的にインストールしますか？ (初回のみ必要です)。",
	chromiumInstallButton: "Microsoft Edge をインストール",
	chromiumInstalling: "Microsoft Edge をインストール中...",
	chromiumInstalled: "Microsoft Edge がインストールされました！",
	chromiumAlreadyInstalled: "互換性のあるブラウザはすでにインストールされています",
	chromiumInstallFailed: "Microsoft Edge のインストールに失敗しました。手動でインストールしてください: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Microsoft Edge のインストールがタイムアウトしました。インターネット接続を確認してください。",
	browserAlreadyInstalled: "互換性のあるブラウザはすでにインストールされています",
	browserInstalled: "Microsoft Edge が正常にインストールされました！",
	browserInstallFailed: "Microsoft Edge のインストールに失敗しました。手動でインストールしてください: flatpak install com.microsoft.Edge",
	flatpakNotFound: "このシステムでは Flatpak が利用できません",
	signInRequired: "プレイするにはログインが必要です",
	syncSkippedNoSubscription: "有効な Xbox Game Pass サブスクリプションが見つかりません。xCloud ライブラリは同期されませんでした。",
	syncSkippedTierUnknown: "Xbox Game Pass サブスクリプションが検出されましたが、プランの確認ができませんでした。利用できないゲームが表示されるのを防ぐため、同期をスキップしました。",
	subscriptionCheckFailed: "Xbox Game Pass サブスクリプションを検証できませんでした。xCloud の同期をスキップしました。接続が安定したら再試行してください。",
	subscriptionDetected: "有効な Xbox Game Pass {{tier}} サブスクリプションを検出しました。"
};
var playButton$4 = {
	play: "プレイ",
	install: "インストール",
	cancel: "キャンセル",
	retry: "再試行",
	downloadFailed: "ダウンロード失敗",
	installing: "インストール中",
	controllerConfig: "コントローラー設定",
	appSettings: "アプリ設定"
};
var play$4 = {
	play: "プレイ",
	resume: "再開",
	stop: "停止",
	cancel: "キャンセル",
	cancelling: "キャンセル中…",
	update: "アップデート",
	uninstall: "アンインストール",
	queued: "待機中",
	downloading: "ダウンロード中",
	extracting: "展開中",
	verifying: "検証中",
	finalizing: "インストール完了処理中",
	cancelConfirmTitle: "ダウンロードをキャンセルしますか？",
	cancelConfirmBody: "{{title}} のダウンロードをキャンセルしてもよろしいですか？一時ダウンロードファイルは削除されます。",
	cancelConfirmConfirm: "ダウンロードをキャンセル",
	cancelConfirmCancel: "ダウンロードを継続",
	playOnCloud: "クラウドでプレイ",
	cloudSave: {
		autoPullOn: "起動時にクラウドセーブが自動的にダウンロードされます。",
		close: "閉じる",
		download: "クラウドセーブをダウンロード",
		filesCount: "{{count}} 個のファイル",
		label: "クラウドセーブ",
		menuTitle: "クラウドセーブ — {{game}}",
		noCloudData: "不明",
		noCloudSupport: "クラウドセーブ非対応",
		setLocation: "セーブデータの場所を設定",
		setLocationHint: "このゲームのセーブデータが保存されるフォルダを選択してください。",
		setLocationTitle: "ゲームのセーブフォルダを選択",
		statusCloudAvailable: "クラウドセーブ利用可能",
		statusNoSaves: "ローカルセーブなし",
		statusSyncing: "クラウドセーブを同期中…",
		statusUnresolved: "セーブ場所が見つかりません — このゲームでは同期が機能しない可能性があります。",
		unresolvedHint: "セーブフォルダを検出するためにゲームを一度起動するか、手動で場所を設定してください。",
		upload: "ローカルセーブをアップロード"
	},
	exe: {
		browse: "参照…",
		changeFailed: "実行ファイルを変更できませんでした",
		changed: "実行ファイルを変更しました",
		defaultTag: "(デフォルト)",
		menuItem: "実行ファイルを変更…",
		reset: "デフォルトの実行ファイルに戻す",
		resetButton: "デフォルトに戻す",
		subtitle: "このゲームが起動するファイルを選択します（ランチャーをスキップしてゲームを直接実行する場合など）。作業ディレクトリとクラウドセーブは影響を受けません。",
		title: "実行ファイルの変更 — {{game}}",
		unavailable: "このゲームのフォルダを読み取れませんでした。"
	}
};
var playMeta$4 = {
	spaceRequired: "必要な空き容量",
	installedSize: "インストールサイズ",
	lastPlayed: "最後にプレイした日",
	neverPlayed: "未プレイ",
	hoursShort: "時間",
	minutesShort: "分",
	played: "プレイ済み"
};
var downloads$4 = {
	title: "ダウンロード",
	empty: "アクティブなダウンロードはありません。ライブラリからゲームをインストールすると、ここに表示されます。",
	current: "ダウンロード中",
	queued: "キューリスト",
	finished: "最近の完了項目",
	cancel: "キャンセル",
	removeFromQueue: "削除",
	play: "プレイ",
	outcome: {
		queued: "待機中",
		downloading: "ダウンロード中",
		completed: "完了",
		cancelled: "キャンセル済み",
		error: "エラー"
	}
};
var navigation$4 = {
	loading: "読み込み中...",
	storesTitle: "ストア連携",
	syncTitle: "ライブラリ同期",
	languageTitle: "言語設定"
};
var security$4 = {
	panelTitle: "セキュリティ",
	counters: {
		label: "カウンター",
		empty: "起動後に発生したイベントはありません"
	},
	log: {
		label: "監査ログ",
		entryCount: "{{count}} 件のログ",
		entryCount_other: "{{count}} 件のログ",
		emptyFiltered: "フィルターに一致するイベントはありません"
	},
	filter: {
		label: "フィルター",
		all: "すべてのイベント"
	},
	timeline: {
		label: "24時間タイムライン ({{count}} 件のイベント)",
		label_other: "24時間タイムライン ({{count}} 件のイベント)",
		axisStart: "-24時間",
		axisEnd: "現在"
	},
	relativeTime: {
		seconds: "{{count}} 秒前",
		minutes: "{{count}} 分前",
		hours: "{{count}} 時間前",
		days: "{{count}} 日前"
	},
	bruteforce: {
		warningTitle: "警告: ブルートフォース攻撃を検出 (警告レベル)",
		escalationTitle: "アラーム: ブルートフォース攻撃を検出 (危険レベル)",
		detail: "過去 {{window}} 秒間に {{failures}} 回の暗号復号化失敗を検出しました。閾値: {{warning}} (警告) / {{escalation}} (危険)。",
		resetButton: "確認して検出器をリセット"
	},
	actions: {
		clearLog: "ログとカウンターをクリア"
	},
	events: {
		tokenEncrypted: "トークンが暗号化されました",
		tokenDecrypted: "トークンが復号されました",
		decryptFailed: "復号に失敗しました",
		tokenFileMigrated: "トークンファイルを移行しました",
		legacyPlaintextDetected: "古い平文テキストを検出しました",
		authFlowStarted: "認証フローが開始されました",
		authFlowCompleted: "認証フローが完了しました",
		authFlowFailed: "認証フローが失敗しました",
		permissionsCheck: "アクセス権限をチェックしました",
		permissionsRepaired: "アクセス権限を修復しました",
		bruteforceSuspected: "ブルートフォース攻撃の疑い",
		deviceResetDetected: "デバイスのリセットを検出しました",
		fingerprintInitialized: "指紋データを初期化しました",
		externalAuthCheckFailed: "外部認証チェックが失敗しました",
		configValidationCompleted: "構成の検証が完了しました",
		configValidationFailed: "構成の検証が失敗しました"
	},
	configValidation: {
		degradedTitle: "構成が無効: 制限モードが有効です",
		detail: "起動時に {{count}} 件 of 構成エラーが検出されました",
		detail_other: "起動時に {{count}} 件 of 構成エラーが検出されました",
		sourceDefaults: "デフォルト値",
		sourceUser: "ユーザー設定",
		sourceMerged: "マージ済み",
		overflow: "+ 他 {{count}} 件のエラー",
		overflow_other: "+ 他 {{count}} 件のエラー"
	}
};
var cloudSyncBehavior$4 = {
	_comment: "各ストアごとにクラウドセーブ同期のエラー表示をどのようにするか選択できる設定パネル。5つの store_label の項目は CloudSyncBehaviorPanel.tsx で描画される行に対応します。Microsoftは意図的に除外されています（xCloudのストリーミングには同期するローカルセーブがないため）。",
	title: "クラウド同期の挙動",
	description: "ゲームの起動時にクラウドセーブのダウンロードまたはアップロードが失敗した場合の Unifideck の挙動を選択します。",
	loadError: "現在の設定を読み込めませんでした",
	saveError: "変更を保存できませんでした。もう一度お試しください",
	store_label: {
		"default": "デフォルト (すべてのストア)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "サイレント (ログ記録のみ)",
		toast: "通知 (ポップアップで表示)"
	}
};
var cloudSync$4 = {
	_comment: "クラウド同期が失敗した際にランチャーから送信されるポップアップ通知。エラー内容は、バックエンドのレスポンスの error_i18n_key フィールドを使用して cloudSync.error.<code> から解決されます。",
	error: {
		network_unreachable: "ネットワークに接続できません",
		auth_expired: "認証の期限が切れました",
		forbidden: "アクセスが禁止されています",
		quota_exceeded: "クラウドストレージの容量制限を超えました",
		server_error: "ストアサーバーが利用できません",
		timed_out: "リクエストがタイムアウトしました",
		disk_full: "ディスク容量が不足しています",
		disk_space_low: "スキップ: ディスク空き容量不足 (空き容量を確保して再試行してください)",
		permission_denied: "アクセス権限がありません",
		cancelled: "キャンセルされました",
		unknown: "予期しないエラーが発生しました"
	},
	unavailableNativeDep: "このシステムではクラウドセーブが無効になっています — Python ({{python}}) に必要なコンポーネントが不足しています。ローカルセーブには影響ありません。同期を復元するには、システムアップデート後にプラグインを再ビルド/再インストールしてください。"
};
var launchLogs$4 = {
	_comment: "LaunchLogsModal 用のメッセージ。このモーダルは、ランチャーの起動失敗（サーキットブレーカーまたはランチャーエラー）時に通知に添付される unifideck://show-logs/<launch_id> リンクから開かれます。~/.local/share/unifideck/launches/<launch_id>.log に保存されている起動ごとのアーカイブされたログを読み込み、ログの重要度に応じて色分けして表示します。",
	title: "起動ログ — {{launchId}}",
	close: "閉じる",
	copyAll: "すべてコピー",
	"export": "ファイルにエクスポート",
	copied: "クリップボードにコピーしました",
	copyFailed: "コピーに失敗しました",
	exporting: "エクスポート中…",
	exportedTo: "{{path}} にエクスポートしました",
	exportFailed: "エクスポートエラー: {{error}}",
	loading: "ログを読み込み中…",
	notAvailable: "この起動ログはすでに削除されています（保存期間の7日を過ぎています）。",
	error: "ログを読み込めませんでした: {{error}}",
	fileInfo: "{{path}} — {{total}} 行中 {{shown}} 行を表示中",
	filterLabel: "表示",
	filterInfo: "情報",
	filterWarning: "警告",
	filterError: "エラー",
	nextError: "次のエラー ({{current}}/{{total}})",
	noErrors: "エラーはありません",
	allFiltered: "現在のフィルターによってすべての行が非表示になっています。ログを表示するには、少なくとも1つの重要度を有効にしてください。"
};
var settings$4 = {
	_comment: "より特定のコンポーネント (LanguageSelector, CloudSyncBehaviorPanel) で処理されない UnifideckSettingsPanel のコントロール。refreshAll は、設定されているすべてのストアで sync_all() を実行するボタンです。ライブラリビューは、自身の SYNC_PROGRESS イベントをサブスクライブして同期状況を表示します。",
	storeConnections: "ストア連携設定",
	librarySync: "ライブラリ同期",
	language: "言語設定",
	languageAuto: "自動検出",
	refreshAll: {
		sectionTitle: "ライブラリのアクション",
		button: "すべてのライブラリを更新",
		buttonBusy: "起動中…",
		startedTitle: "ライブラリの更新を開始しました",
		startedBody: "すべてのストアがバックグラウンドで同期されます。",
		startedSingle: "{{store}} ライブラリをバックグラウンドで更新しています。",
		failedTitle: "ライブラリの更新を開始できませんでした"
	}
};
var saveFolder$4 = {
	_comment: "SaveFolderModal 用のメッセージ。このモーダルは、通常クラウド同期が失敗した際にローカルの保存内容を確認したい場合、unifideck://open-save-folder/<store>/<game_id> リンクから開かれます。count と size は変数が入ります。truncated はリストが省略されている場合にのみ表示されます。",
	title: "セーブフォルダ — {{store}} / {{gameId}}",
	close: "閉じる",
	copyPath: "パスをコピー",
	copyFileList: "ファイル一覧をコピー",
	depthLabel: "階層の深さ",
	filterLabel: "フィルター",
	loading: "フォルダ内容を読み込み中…",
	noSavesYet: "このゲームのローカルセーブデータがまだ見つかりません。フォルダは次回のクラウド同期成功時に作成されます。",
	aggregate: "ファイル数: {{count}}、合計サイズ: {{size}}",
	truncated: "他 {{count}} 個のファイル ({{size}}) は表示されていません。フィルターで絞り込んでください。",
	error: "セーブフォルダを読み込めませんでした: {{error}}"
};
var library$4 = {
	_comment: "エラーが繰り返された場合の強制起動およびエラーリセットコントロールを含む、ライブラリビュー内のゲームごとの UI サービス。",
	circuitBreaker: {
		_comment: "起動エラー時のサーキットブレーカーUI。設定した時間内のエラー数が circuit_breaker.failures_threshold (デフォルトでは600秒間に3回) に達した場合のみバッジが表示されます。強制起動は制限を1回のみバイパスします。リセットはすべてのエラー履歴をクリアします。",
		badgeRecent: "{{count}} 回の最近の起動失敗",
		badgeRecent_other: "{{count}} 回の最近の起動失敗",
		badgeTooltip: "最近 {{count}} 回の起動に失敗しました。サーキットブレーカーにより自動起動が停止されています。",
		forceLaunchButton: "強制起動",
		resetButton: "エラー履歴をクリア",
		failureKind: {
			fastBoot: "高速起動時のクラッシュ",
			launcherError: "ランチャー準備エラー"
		}
	}
};
var pickStorage$4 = {
	title: "インストール先",
	customOption: "別のフォルダを選択...",
	customLabel: "カスタムフォルダ",
	change: "変更...",
	gameSize: "インストールサイズ"
};
var restart$4 = {
	title: "同期が完了しました！",
	body: "新しいゲームとアートワークを反映するには、Steam を再起動してください",
	confirm: "今すぐ再起動",
	later: "後で"
};
var updater$4 = {
	checkButton: "アップデートを確認",
	checkCompleteMessage: "最新のバージョン情報を取得しました。",
	checkCompleteTitle: "アップデート確認完了",
	checkFailedTitle: "確認失敗",
	checkingButton: "確認中...",
	currentTitle: "現在",
	downgradeButton: "v{{version}} にダウングレード",
	installFailedTitle: "インストール失敗",
	installTimeoutMessage: "Decky Loader から応答がありません。インストールが停止したか失敗した可能性があります。もう一度お試しください。",
	installedLabel: "インストール済み",
	installingButton: "インストール中...",
	installingTitle: "プラグインのインストール",
	latestLabel: "最新",
	modalTitle: "UNIFIDECK v{{version}} — リリースノート",
	noBackend: "Decky バックエンドが利用できません。",
	noReleaseNotes: "リリースノートはありません。",
	reinstallButton: "v{{version}} を再インストール",
	refreshFailedMessage: "最新バージョンの情報を確認できませんでした。接続を確認してもう一度お試しください。",
	releaseGoneMessage: "このリリースは現在利用できません。アップデートを確認して、もう一度選択してください。",
	releaseNotesButton: "リリースノート",
	stageDownload: "ダウンロード中…",
	stageFinish: "完了しています…",
	stageInstall: "インストール中…",
	stageOpen: "パッケージ読み込み中…",
	stageParse: "検証中…",
	stageRemove: "旧バージョンを削除中…",
	stageStart: "起動中…",
	stageWorking: "インストール中…",
	titleLoading: "バージョンを確認中",
	typeDowngrade: "以下へダウングレード:",
	typeReinstall: "再インストール",
	typeUpdate: "以下へアップデート:",
	updateButton: "v{{version}} にアップデート"
};
var achievements$4 = {
	unlockedTitle: "実績を解除しました",
	unlockedBody: "{{name}}"
};
var achievementsModal$4 = {
	title: "実績",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "更新",
	close: "閉じる",
	loading: "実績を読み込んでいます…",
	empty: "このゲームには実績がありません。",
	hidden: "隠し実績",
	unlockedOn: "{{date}} に解除",
	error: "実績を読み込めませんでした。",
	errorOffline: "実績を表示するにはインターネットに接続してください。",
	errorAuth: "実績を表示するには、このゲームのストアにサインインしてください。",
	errorUnavailable: "このゲームでは実績を利用できません。"
};
var collectionSettings$4 = {
	title: "STEAM コレクション",
	enabled: "コレクションが有効",
	disabled: "コレクションが無効"
};
var jaJP = {
	_comment: _comment$4,
	tabs: tabs$4,
	auth: auth$4,
	storage: storage$4,
	storageSettings: storageSettings$4,
	gameGrid: gameGrid$4,
	downloadsTab: downloadsTab$4,
	cloudSave: cloudSave$4,
	storeConnections: storeConnections$4,
	librarySync: librarySync$4,
	cleanup: cleanup$4,
	captureLogs: captureLogs$4,
	installButton: installButton$4,
	confirmModals: confirmModals$4,
	toasts: toasts$4,
	uninstallModal: uninstallModal$4,
	languageSettings: languageSettings$4,
	errors: errors$4,
	sync: sync$4,
	force_sync: force_sync$4,
	artwork: artwork$4,
	deckTabs: deckTabs$4,
	unifiedLibrary: unifiedLibrary$4,
	gameInfoPanel: gameInfoPanel$4,
	gameDetailsSettings: gameDetailsSettings$4,
	gogLanguageModal: gogLanguageModal$4,
	accountSwitch: accountSwitch$4,
	ubisoftAuth: ubisoftAuth$4,
	relativeDate: relativeDate$4,
	common: common$4,
	info: info$4,
	microsoft: microsoft$4,
	playButton: playButton$4,
	play: play$4,
	playMeta: playMeta$4,
	downloads: downloads$4,
	navigation: navigation$4,
	security: security$4,
	cloudSyncBehavior: cloudSyncBehavior$4,
	cloudSync: cloudSync$4,
	launchLogs: launchLogs$4,
	settings: settings$4,
	saveFolder: saveFolder$4,
	library: library$4,
	pickStorage: pickStorage$4,
	restart: restart$4,
	updater: updater$4,
	achievements: achievements$4,
	achievementsModal: achievementsModal$4,
	collectionSettings: collectionSettings$4
};

var _comment$3 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$3 = {
	settings: "설정",
	downloads: "다운로드"
};
var auth$3 = {
	connect: "연동하기",
	disconnect: "연동 해제",
	status: {
		connected: "연동됨",
		disconnected: "연동 안 됨",
		checking: "확인 중…",
		error: "오류 — 로그 확인 필요"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$3 = {
	title: "저장 공간",
	defaultUpdated: "기본 저장 경로가 업데이트되었습니다",
	isDefault: "기본",
	setDefault: "기본으로 설정",
	customPath: "사용자 지정 경로",
	up: "↑ 상위",
	refresh: "새로고침",
	useThisPath: "이 경로 사용"
};
var storageSettings$3 = {
	title: "다운로드 설정",
	installLocationLabel: "설치 위치",
	installLocationDescription: "새로운 게임을 다운로드할 위치",
	pathLabel: "경로",
	loading: "저장소 옵션 불러오는 중...",
	toastUpdatedTitle: "저장 위치 업데이트됨",
	toastUpdatedBody: "새로운 게임이 {{location}}에 설치됩니다",
	toastFailedTitle: "업데이트 실패",
	toastFailedBody: "{{error}}",
	unknown: "알 수 없음",
	browseButton: "찾아보기...",
	clearCustom: "지정 위치 초기화",
	customPathSet: "사용자 지정 설치 위치 설정됨",
	newFolder: "새 폴더",
	newFolderCreate: "생성",
	selectFolder: "이 폴더 사용",
	emptyDirectory: "빈 폴더",
	showHidden: "숨김 파일 표시",
	selectDevice: "장치 선택",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "수정일 (최신순)",
	sortModifiedOldest: "수정일 (오래된순)",
	sortCreatedNewest: "생성일 (최신순)",
	sortCreatedOldest: "생성일 (오래된순)",
	sortSizeLargest: "크기 (큰순)",
	sortSizeSmallest: "크기 (작은순)",
	"validationErrors.notAbsolute": "경로는 절대 경로여야 합니다",
	"validationErrors.systemDir": "시스템 디렉토리는 사용할 수 없습니다",
	"validationErrors.notWritable": "해당 위치에 쓰기 권한이 없습니다",
	"validationErrors.notFound": "디렉토리가 존재하지 않습니다",
	"validationErrors.homeRoot": "사용자 루트 경로는 사용할 수 없습니다",
	freeSpaceLabel: "{{label}} (여유 공간 {{freeSpace}} GB)"
};
var gameGrid$3 = {
	loading: "게임 불러오는 중...",
	noGamesFound: "게임을 찾을 수 없습니다",
	noGamesHint: "Unifideck 설정에서 라이브러리 동기화를 진행해 보세요",
	showingGames: "{{count}}개의 게임 표시 중",
	installed: "설치됨",
	notInstalled: "설치되지 않음",
	playtime: "플레이 시간: {{hours}}시간 {{minutes}}분"
};
var downloadsTab$3 = {
	currentDownload: "진행 중인 다운로드",
	queuedDownloads: "대기열 ({{count}})",
	recentlyCompleted: "최근 완료됨",
	noActiveDownloads: "활성화된 다운로드 없음",
	noDownloads: "다운로드가 없습니다. 라이브러리에서 게임을 설치하면 여기에 표시됩니다.",
	loadingLabel: "불러오는 중...",
	loadingMessage: "다운로드 대기열 확인 중...",
	confirmCancelTitle: "취소 확인",
	confirmCancelDescription: "정말로 {{game}} 다운로드를 취소하시겠습니까?",
	confirmCancelYes: "예",
	confirmCancelNo: "아니요",
	cancelDownload: "취소",
	toastDownloadCancelledTitle: "다운로드 취소됨",
	toastDownloadCancelledBody: "다운로드가 대기열에서 제거되었습니다.",
	toastCancelFailedTitle: "취소 실패",
	toastCancelFailedBody: "다운로드를 취소할 수 없습니다: {{error}}",
	status: {
		queued: "대기 중",
		downloading: "다운로드 중",
		completed: "완료",
		cancelled: "취소됨",
		error: "오류"
	},
	preparingDownload: "다운로드 준비 중...",
	phaseExtracting: "{{game}} 압축 해제 중...",
	phaseVerifying: "{{game}} 무결성 검사 중...",
	internalStorage: "내장 저장소 (여유 공간 {{freeSpace}} GB)",
	sdCard: "SD 카드 (여유 공간 {{freeSpace}} GB)",
	customLocation: "사용자 지정: {{path}} (여유 공간 {{freeSpace}} GB)",
	extractingLabel: "압축 해제 중...",
	verifyingLabel: "검사 중...",
	updateQueuedLabel: "업데이트 대기 중",
	downloadQueuedLabel: "다운로드 대기 중",
	downloadingUpdateLabel: "업데이트 다운로드 중",
	downloadingLabel: "다운로드 중...",
	finalizingInstallation: "설치 완료하는 중...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "남은 시간: {{eta}}",
	preparingMessage: "접두사 초기화 및 클라우드 저장 데이터 동기화 중...",
	preparingLabel: "게임 설정 중...",
	installingViaUpcLabel: "Ubisoft Connect로 설치 중",
	extractingMessage: "파일 압축 해제 중...",
	verifyingMessage: "파일 확인 중... {{pct}}%",
	installingViaUpcMessage: "Ubisoft Connect 창에 따라 설치를 완료하세요..."
};
var cloudSave$3 = {
	title: "클라우드 저장 데이터 충돌",
	description: "{{game}}의 로컬 저장 데이터와 클라우드 데이터가 일치하지 않습니다",
	local: "로컬:",
	cloud: "클라우드:",
	newer: "(최신)",
	useCloud: "클라우드 데이터 사용",
	useLocal: "로컬 데이터 사용",
	unknownDate: "알 수 없음",
	toastUsingCloudTitle: "클라우드 데이터 적용",
	toastUsingCloudBody: "{{game}}의 클라우드 저장 데이터를 다운로드하는 중",
	toastUsingLocalTitle: "로컬 데이터 적용",
	toastUsingLocalBody: "{{game}}의 로컬 저장 데이터를 클라우드에 업로드하는 중",
	toastErrorTitle: "오류",
	toastErrorBody: "충돌 해결 실패",
	filesSize: "{{count}}개 파일 · {{size}}",
	uploadBlockedEmpty: "{{game}}의 클라우드 업로드를 건너뛰었습니다 — 로컬 저장 파일을 찾을 수 없습니다",
	uploadSkippedTitle: "클라우드 업로드 건너뜀",
	uploadSkippedBody: "{{game}}에 업로드할 로컬 저장 파일이 아직 없습니다."
};
var storeConnections$3 = {
	title: "스토어 연동",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "연동됨",
	checking: "확인 중...",
	authenticate: "로그인",
	legendaryNotInstalled: "⚠️ Epic Games 툴 설치 중...",
	nileNotInstalled: "⚠️ Amazon Games 툴을 찾을 수 없음",
	error: "오류 - 로그 확인 필요"
};
var librarySync$3 = {
	title: "라이브러리 동기화",
	syncLibraries: "라이브러리 동기화",
	forceSync: "강제 동기화",
	syncing: "동기화 중...",
	cancelSync: "동기화 취소",
	retryStatusCheck: "상태 확인 재시도",
	gamesSynced: "{{synced}} / {{total}}개의 게임 동기화됨",
	artworkDownloaded: "{{synced}} / {{total}}개의 이미지 다운로드됨",
	metadataExtracted: "{{synced}} / {{total}}개의 메타데이터 추출됨",
	steamMetadataDownloaded: "Steam 메타데이터: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDB 메타데이터: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacritic 데이터: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWG 메타데이터: {{synced}} / {{total}}",
	compatChecked: "호환성 정보: {{synced}} / {{total}}",
	cancelling: "취소 중…",
	error: "오류"
};
var cleanup$3 = {
	title: "정리 작업",
	deleteAll: "모든 UNIFIDECK 라이브러리 및 캐시 삭제",
	modalTitle: "모든 Unifideck 데이터를 삭제하시겠습니까?",
	modalDescription: "이렇게 하면 모든 Unifideck 바로가기, Steam 라이브러리 이미지, 저장된 스토어 로그인 정보 및 캐시 데이터가 삭제됩니다. 설치된 게임 파일은 유지되므로 언제든 다시 동기화할 수 있습니다. 설치된 게임의 로컬 파일까지 함께 삭제하려면 아래 파괴적 모드를 활성화하세요.",
	deleteFilesLabel: "설치된 게임 파일도 삭제 (파괴적 모드)",
	deleteFilesDescription: "안전 루트 경로 하위에 설치된 모든 Unifideck 게임 폴더를 디스크에서 삭제합니다.",
	confirmDelete: "예, 모두 삭제합니다",
	cancel: "취소",
	deleting: "삭제 중..."
};
var captureLogs$3 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (ko-KR).",
	title: "로그 수집",
	description: "모든 Unifideck 로그를 하나의 ZIP 파일로 모아 다운로드 폴더에 저장합니다. 버그 신고에 첨부할 수 있습니다.",
	capture: "로그 수집",
	capturing: "로그 수집 중...",
	savedTo: "다운로드 폴더에 저장되었습니다",
	bundleInfo: "파일 {{files}}개 · {{size}}",
	partialNote: "{{count}}개 파일을 읽을 수 없었습니다. 그래도 이 묶음은 사용할 수 있습니다.",
	partialNote_other: "{{count}}개 파일을 읽을 수 없었습니다. 그래도 이 묶음은 사용할 수 있습니다.",
	copyPath: "경로 복사",
	modalTitle: "진단 묶음을 만들까요?",
	modalDescription: "Unifideck이 로그를 하나의 ZIP 파일로 모아 다운로드 폴더에 저장합니다. 버그 신고에 첨부할 수 있습니다.",
	modalIncludes: "포함되는 항목: 플러그인 및 게임 실행 로그, 동기화와 설치 기록, 게임 제목과 설치 경로, 모델·운영체제·저장장치 등 기기 정보.",
	modalExcludes: "절대 포함되지 않는 항목: 비밀번호, 스토어 로그인 토큰, 브라우저 쿠키.",
	modalConfirm: "묶음 만들기",
	cancel: "취소"
};
var installButton$3 = {
	play: "플레이",
	resume: "이어하기",
	install: "{{title}} 설치",
	installNative: "설치",
	uninstall: "{{title}} 설치 제거",
	cancel: "✖ 취소",
	processing: "진행 중...",
	checking: "확인 중...",
	size: "{{size}} GB",
	update: "업데이트",
	updateAvailable: "업데이트 가능",
	updateAvailableDescription: "새 버전 설치 준비가 완료되었습니다."
};
var confirmModals$3 = {
	installTitle: "설치 확인",
	installDescription: "정말로 {{title}}을(를) 설치하시겠습니까?",
	uninstallTitle: "설치 제거 확인",
	uninstallDescription: "정말로 {{title}}을(를) 설치 제거하시겠습니까?",
	cancelTitle: "취소 확인",
	cancelDescription: "정말로 {{title}} 다운로드를 취소하시겠습니까?",
	forceSyncTitle: "강제 동기화 설정",
	forceSyncDescription: "모든 게임 이미지를 다시 동기화하시겠습니까? 수동으로 변경한 이미지 정보가 초기화됩니다. 누락된 이미지만 채우려면 '이미지 유지'를 선택해 주세요.",
	resyncArtwork: "이미지 전체 다시 동기화",
	keepArtwork: "이미지 유지",
	steamRestartTitle: "동기화 완료!",
	steamRestartDescription: "새로운 게임과 이미지를 적용하려면 Steam을 재시작해 주세요",
	steamRestartDescriptionStore: "새로운 게임과 {{store}} 이미지를 적용하려면 Steam을 재시작해 주세요",
	steamRestartCleanupTitle: "정리 완료!",
	steamRestartCleanupDescription: "삭제된 게임과 이미지를 완전히 정리하려면 Steam을 재시작해 주세요",
	restartNow: "지금 재시작",
	later: "나중에",
	ubisoftInstallTitle: "Ubisoft Connect 필요",
	ubisoftInstallDescription: "{{title}} 설치를 위해 필요한 Ubisoft Connect 런처를 실행합니다.",
	ubisoftInstallConfirm: "Ubisoft Connect 열기",
	yes: "예",
	no: "아니요",
	amazonUpdateTitle: "전체 재다운로드 필요",
	amazonUpdateDescription: "Amazon 게임 업데이트는 {{title}}의 전체 데이터를 다시 다운로드해야 합니다. 시간이 다소 걸릴 수 있습니다. 진행하시겠습니까?",
	updateConfirm: "업데이트"
};
var toasts$3 = {
	downloadStarted: "다운로드 시작됨. UNIFIDECK > 다운로드 탭을 확인하세요",
	downloadQueued: "다운로드 대기 중",
	downloadQueuedBody: "{{title}}이(가) 대기열에 추가되었습니다.",
	downloadFailedUnknown: "알 수 없는 오류입니다.",
	updateQueued: "업데이트 대기 중",
	uninstallDone: "설치 제거 완료.",
	ubisoftLauncherOpening: "Ubisoft Connect 여는 중",
	ubisoftLauncherOpeningMessage: "{{title}} 처리를 위해 Ubisoft Connect를 실행하는 중입니다. 스토어 내에서 게임을 설치하고 여기로 돌아와 실행해 주세요.",
	ubisoftLauncherAlreadyOpenMessage: "이 설치 대상에 대해 이미 Ubisoft Connect가 열려 있습니다.",
	ubisoftLauncherOpenFailed: "Ubisoft Connect를 열지 못했습니다",
	ubisoftLauncherOpenFailedMessage: "설치를 위한 Ubisoft Connect 실행에 실패했습니다.",
	multipartDetected: "분할 다운로드 감지됨",
	multipartMessage: "완료될 때까지 잠시 기다려 주세요",
	downloadFailed: "다운로드 실패",
	downloadFailedMessage: "다운로드를 시작할 수 없습니다.",
	downloadCancelled: "다운로드 취소됨",
	downloadCancelledMessage: "{{title}} 다운로드가 취소되었습니다.",
	cancelFailed: "취소 실패",
	cancelFailedMessage: "다운로드를 취소할 수 없습니다.",
	uninstalling: "설치 제거 중...",
	uninstallingMessage: "{{title}} 설치 제거 중...",
	uninstallingMessageProton: "{{title}} 및 Proton 파일 삭제 중...",
	uninstallComplete: "설치 제거 완료",
	uninstallCompleteMessage: "{{title}} 삭제됨.",
	uninstallCompleteMessageProton: "{{title}} 및 관련 Proton 데이터가 모두 삭제되었습니다.",
	installComplete: "설치 완료!",
	installCompleteMessage: "{{title}}을(를) 플레이할 수 있습니다.",
	syncComplete: "동기화 완료!",
	syncCompleteMessage: "{{count}}개의 게임이 추가되었습니다. 스팀 라이브러리에 반영하려면 STEAM을 완전히 재시작(단순히 게이밍 모드로 돌아가는 게 아니라 Steam 종료 후 재실행)해 주세요.",
	forceSyncComplete: "강제 동기화 완료!",
	forceSyncCompleteMessage: "{{count}}개의 게임 정보가 업데이트되었습니다. 변경 사항을 반영하려면 STEAM을 재시작해 주세요.",
	syncCancelled: "동기화 취소됨",
	authTimeout: "인증 시간 초과",
	authTimeoutMessage: "{{store}} 연동 인증이 만료되었습니다. 다시 시도해 주세요.",
	authFailed: "인증 실패",
	authFailedMessage: "연동 인증 프로세스를 시작할 수 없습니다",
	authError: "인증 오류",
	authConnected: "{{store}} 연동됨!",
	authConnectedMessage: "{{store}} 인증에 성공했습니다",
	cleanupStarted: "정리 작업 시작됨",
	cleanupStartedMessage: "바로가기, 이미지, 저장된 로그인 정보를 지우는 중입니다. 잠시만 기다려 주세요…",
	cleanupSuccessful: "정리 완료",
	cleanupSuccessfulMessage: "바로가기 {{games}}개, 이미지 {{artwork}}개 제거, 연동 스토어 {{stores}}개 해제, 설치 폴더 {{files}}개 삭제. 캐시가 완전히 비워졌습니다.",
	cleanupNoop: "정리할 대상 없음",
	cleanupNoopMessage: "삭제할 Unifideck 바로가기, 이미지 또는 로그인 세션 정보가 없습니다.",
	deleteFailed: "삭제 실패",
	repairPrefixSuccess: "접두사(Prefix) 복구 완료",
	repairPrefixSuccessMessage: "Ubisoft Connect 실행 환경(Prefix)이 재구성되었습니다",
	repairPrefixFailed: "복구 실패",
	repairPrefixFailedMessage: "실행 환경을 복구하지 못했습니다: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "네트워크 오류",
		launchRetry: "실행 재시도 중",
		retryingUmu: "{{seconds}}초 후 UMU 재시도 (시도 횟수 {{attempt}}/{{max}})...",
		protonUpgrade: "Proton 업그레이드",
		resettingPrefix: "{{version}} 환경 재조정 중...",
		firstTimeSetup: "초기 설정 진행",
		initializingPrefix: "Wine prefix 초기화 중...",
		setupCompleteTitle: "설정 완료",
		prefixInitialized: "Wine prefix 초기화됨",
		setupFallback: "설정 진행",
		fallbackInitialization: "대체 초기화 처리 중...",
		launchingGame: "게임 실행 중",
		startingEpicGame: "Epic 게임 시작 중...",
		startingAmazonGame: "Amazon 게임 시작 중...",
		startingGogGame: "GOG 게임 시작 중...",
		startingUbisoftGame: "Ubisoft 게임 시작 중...",
		dependenciesTitle: "종속성 설치 중",
		installingRedistributables: "게임 실행 필요 구성요소 설치 중...",
		dependenciesReady: "종속성 설치 완료",
		redistributablesInstalled: "필수 구성요소 설치됨",
		dependenciesStatus: "필수 파일 확인",
		checkLogs: "설정 완료 (자세한 내용은 로그를 확인하세요)",
		installingProton: "Proton 설치 중",
		attemptingInstall: "{{version}} 설치 시도 중...",
		epicAuthRequired: "Epic 로그인 필요",
		epicAuthRequiredMessage: "Unifideck 설정에서 Epic Games 계정으로 로그인해 주세요",
		installingRedist: "공용 런타임 설치",
		installingRedistMessage: "GOG 공용 런타임 초기 설정 진행 중...",
		setupFailed: "설정 실패",
		setupFailedMessage: "설치에 실패했습니다. 로그를 통해 세부 정보를 확인하세요",
		prerequisitesTitle: "필수 구성요소 설치",
		installingPrerequisites: "필수 실행 구성요소를 설치하는 중...",
		prerequisitesReady: "필수 구성요소 준비 완료",
		prerequisitesInstalled: "필수 실행 구성요소 설치 완료",
		prerequisitesFailed: "필수 구성요소 경고",
		prerequisitesFailedMessage: "필수 구성요소가 정상적으로 설치되지 않았을 수 있습니다",
		browserRequired: "Microsoft Edge 필요",
		signingIn: "로그인 중...",
		cloudSyncDownFailed: "{{store}} 클라우드 세이브 다운로드 실패 ({{error}}). 로컬 저장 데이터로 실행합니다.",
		cloudSyncUpFailed: "{{store}} 클라우드 세이브 업로드 실패 ({{error}}). 게임 진행 상태가 로컬에만 보존됩니다.",
		errorCircuitBreakerOpen: "최근 {{game_key}} 실행 실패가 {{count}}회 누적되어 자동 실행이 일시 정지되었습니다.",
		launcherError: "{{game_key}}을(를) 실행할 수 없습니다 ({{error_code}}). 로그를 확인해 주세요.",
		downloadingProton: "Proton 다운로드 중 — 몇 분 정도 걸릴 수 있습니다...",
		downloadingRuntime: "Steam Runtime 다운로드 중 — 첫 실행은 몇 분 정도 걸릴 수 있습니다...",
		installingUbisoft: "Ubisoft Connect로 설치 중",
		installingUbisoftMessage: "Ubisoft Connect를 여는 중 — 런처 창에서 설치를 시작하세요.",
		protonReadyBody: "{{version}}을(를) 사용할 준비가 되었습니다",
		protonReadyTitle: "Proton 준비 완료",
		protonSwitchedTo: "이제 {{version}} 사용 중",
		signingInUbisoft: "Ubisoft Connect에 로그인 중",
		signingInUbisoftMessage: "Ubisoft Connect를 여는 중 — 거기서 로그인한 다음 돌아오세요.",
		ubisoftPrefixNotReady: "Ubisoft Connect가 준비되지 않았습니다",
		ubisoftPrefixNotReadyMessage: "이 게임의 Ubisoft Connect 프리픽스를 준비하지 못했습니다. 다시 설치해 보세요. 계속 실패하면 라이브러리 동기화를 다시 실행하세요.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect가 준비되지 않았습니다",
		ubisoftAuthPrefixNotReadyMessage: "로그인을 위해 Ubisoft Connect를 준비하지 못했습니다. 잠시 후 다시 시도해 주세요.",
		ubisoftLaunchIdMissing: "Ubisoft Connect를 여는 중",
		ubisoftLaunchIdMissingMessage: "이 게임의 실행 ID를 확인할 수 없습니다 — 라이브러리에서 시작할 수 있도록 Ubisoft Connect를 여는 중입니다. 직접 실행을 수정하려면 라이브러리 동기화를 다시 실행하세요."
	},
	updateComplete: "업데이트 완료!",
	updateCompleteMessage: "{{title}} 업데이트가 완료되었습니다.",
	updateCancelled: "업데이트 취소됨",
	updateCancelledMessage: "{{title}} 업데이트가 취소되었습니다.",
	updateFailed: "업데이트 실패",
	updateFailedMessage: "업데이트 작업을 시작할 수 없습니다.",
	storeError: "{{store}} 연동 서버와 통신할 수 없습니다 ({{errType}})",
	captureLogsStarted: "로그 수집 중...",
	captureLogsStartedMessage: "다운로드 폴더에 진단 묶음을 만들고 있습니다. 몇 초 걸립니다.",
	captureLogsDone: "로그를 수집했습니다",
	captureLogsDoneMessage: "{{path}}에 저장했습니다 ({{size}}, 파일 {{files}}개).",
	captureLogsPartial: "로그를 수집했습니다 (일부)",
	captureLogsPartialMessage: "{{path}}에 저장했습니다 ({{size}}, 파일 {{files}}개). {{failures}}개 파일을 읽을 수 없었습니다.",
	captureLogsFailed: "로그를 수집할 수 없습니다",
	captureLogsBusy: "이미 로그 수집이 진행 중입니다",
	captureLogsCopied: "경로를 복사했습니다",
	captureLogsCopyFailed: "경로를 복사할 수 없습니다",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "여유 공간 확보",
		signInToStore: "로그인",
		retrySync: "재시도",
		retryLibrarySync: "라이브러리 새로고침",
		openSaveFolder: "세이브 폴더 열기",
		showLogs: "로그 보기"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "{{store}} 라이브러리 새로고침 실패 ({{error}}). 기존 라이브러리 목록이 표시됩니다."
	},
	cloudPullStarted: "클라우드 저장 데이터 다운로드 중…",
	cloudPushStarted: "로컬 저장 데이터 업로드 중…",
	cloudSavePathFailed: "저장 위치를 설정할 수 없음",
	cloudSavePathSet: "저장 위치 업데이트됨",
	cloudPullDone: "클라우드 저장 데이터 다운로드 완료",
	cloudPullFailed: "클라우드 저장 데이터 다운로드 실패",
	cloudPushDone: "로컬 저장 데이터 업로드 완료",
	cloudPushFailed: "로컬 저장 데이터 업로드 실패"
};
var uninstallModal$3 = {
	title: "설치 제거 확인",
	description: "정말로 {{title}}을(를) 설치 제거하시겠습니까?",
	deleteProtonLabel: "관련 Proton 데이터도 삭제",
	deleteProtonDescription: "호환성 데이터 및 사용자 파일을 완전히 삭제합니다",
	warningTitle: "주의 사항:",
	warningBody: "이 작업은 Wine prefix 내에 있는 로컬 세이브 데이터를 영구적으로 지웁니다. 필요 시 미리 백업을 진행해 주세요.",
	cancel: "취소",
	uninstall: "설치 제거"
};
var languageSettings$3 = {
	title: "언어 설정",
	label: "표시 언어",
	description: "인터페이스 및 게임 설치 시 지원되는 다운로드 언어",
	autoDetect: "자동 (시스템 설정 기준)"
};
var errors$3 = {
	legendaryNotFound: "Epic Games CLI (Legendary)를 찾을 수 없음",
	gogdlNotFound: "GOG CLI (gogdl)를 찾을 수 없음",
	nileNotFound: "Amazon Games CLI (Nile)를 찾을 수 없음",
	authUrlFailed: "연동 인증용 주소를 가져오지 못했습니다",
	authFailed: "연동 인증 실패",
	notAuthenticated: "연동되지 않았습니다. 로그인 설정을 진행해 주세요.",
	syncInProgress: "동기화가 진행 중입니다",
	syncCancelled: "사용자에 의해 동기화가 취소되었습니다",
	gameNotFound: "게임을 찾을 수 없습니다",
	gameNotInstalled: "게임이 설치되어 있지 않습니다",
	installFailed: "설치 실패 - 세부 정보는 로그를 참조하세요",
	installNoDirectory: "설치는 완료되었으나 게임 설치 경로를 찾을 수 없습니다",
	uninstallFailed: "설치 제거 실패",
	lockConflict: "락 충돌 발생 - 잠시 후 다시 시도해 주세요",
	unknownStore: "알 수 없는 스토어",
	shortcutWriteFailed: "바로가기 정보 기록 실패",
	backgroundSyncDisabled: "백그라운드 동기화 비활성화됨",
	steamGridDbUnavailable: "SteamGridDB 서비스를 사용할 수 없습니다",
	deleteSyncInProgress: "동기화가 진행 중일 때는 지울 수 없습니다",
	alreadyInQueue: "이미 대기열에 존재합니다",
	download: {
		diskSpace: "사용 가능한 디스크 공간 부족",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "네트워크 연결 오류 — 연결 상태를 확인하세요",
		authExpired: "로그인 세션 만료 — 다시 로그인해 주세요",
		toolMissing: "필수 다운로드 도구를 찾을 수 없습니다",
		processFailed: "다운로드 실패 — 다시 시도해 주세요",
		directoryNotFound: "설치 폴더 위치를 파악할 수 없습니다",
		lockConflict: "다른 작업이 진행 중입니다 — 잠시 후 재시도",
		ubisoftClient: "Ubisoft Connect 클라이언트 오류 — Prefix 복구를 시도해 보세요",
		ubisoftClientNotRunning: "Ubisoft Connect 클라이언트가 응답하지 않습니다",
		ubisoftBootstrap: "Ubisoft Connect 실행 환경(Prefix)을 설정하지 못했습니다",
		ubisoftUmuMissing: "umu-run 파일을 찾을 수 없음 — Ubisoft 게임 작동 필요",
		generic: "설치 실패 — 세부 정보는 로그를 참조하세요"
	},
	unknown: "알 수 없는 오류",
	steamAppsUnavailable: "SteamClient.Apps API를 사용할 수 없습니다"
};
var sync$3 = {
	start: "라이브러리 동기화",
	force: "강제 동기화",
	syncing: "동기화 중…",
	cancel: "동기화 취소",
	cancelling: "취소 중…",
	preparing: "준비 중…",
	downloadingArtwork: "{{game}} 이미지 다운로드 중...",
	fetchingGameLists: "게임 목록 받아오는 중...",
	fetchingStore: "{{store}} 정보 불러오는 중 ({{current}}/{{total}})...",
	cancelledByUser: "사용자에 의해 동기화가 취소되었습니다",
	checkingInstalledGames: "설치된 게임 폴더 확인 중...",
	sgdbLookup: "SteamGridDB에서 {{count}}개의 게임 검색 중...",
	extractingSteamMetadata: "Steam 메타데이터 추출 중...",
	fetchingEnhancedMetadata: "추가 메타데이터 받아오는 중...",
	lookingUpUnifiDB: "unifiDB 메타데이터 받아오는 중...",
	fetchingMetacriticData: "Metacritic 데이터 받아오는 중...",
	fetchingCompatData: "호환성 검증 진행 중...",
	checkingExistingArtwork: "기존 이미지 확인 중...",
	cleaningOrphanedArtwork: "정리되지 않은 임시 이미지 정리 중...",
	retryingMissingArtwork: "이미지가 누락된 {{synced}} / {{total}}개의 게임 재시도 중...",
	savingShortcuts: "바로가기 저장 중...",
	completed: "동기화 완료!",
	extractingMetadata: "{{game}} 메타데이터 추출 중...",
	cooldown: "{{secs}}초 동안 대기",
	artworkPhase: "이미지 {{done}} / {{total}}"
};
var force_sync$3 = {
	migratingOldInstallations: "강제 동기화: 기존 설치 정보 마이그레이션 중...",
	fetchingGameLists: "강제 동기화: 게임 목록 조회 중...",
	checkingInstalledGames: "강제 동기화: 설치된 게임 확인 중...",
	rewritingShortcuts: "강제 동기화: 바로가기 재생성 중...",
	cancelledShortcutsSaved: "강제 동기화 취소됨 - 기존 정보 보존",
	writingShortcuts: "강제 동기화: 바로가기 정보 파일 기록 중...",
	clearingProton: "강제 동기화: Proton 캐시 및 설정 정보 제거 중...",
	completed: "강제 동기화 완료! {{updated}}개 게임 업데이트됨, 이미지 {{artwork}}개 갱신 완료."
};
var artwork$3 = {
	checking: "이미지 무결성 검증 중...",
	queueRefresh: "이미지 새로고침을 위해 대기열 등록 중...",
	downloadProgress: "이미지 다운로드 진행 상황: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$3 = {
	greatOnDeck: "Deck 추천 게임",
	allGames: "모든 게임",
	installed: "설치됨",
	nonSteam: "비 Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$3 = {
	error: "Unifideck 오류",
	errorLoadingView: "통합 라이브러리 화면을 불러올 수 없습니다",
	checkConsole: "세부 정보는 브라우저 콘솔을 확인해 주세요",
	errorLoadingGames: "게임 목록 로딩 오류",
	tryReloading: "플러그인을 다시 실행하거나 콘솔을 확인해 보세요",
	storeLabel: "스토어:",
	allStores: "모든 스토어",
	searchPlaceholder: "게임 검색...",
	library: "라이브러리"
};
var gameInfoPanel$3 = {
	loading: "게임 정보 불러오는 중...",
	noMetadata: "이 게임에 사용할 수 있는 메타데이터가 없습니다.",
	noTestResults: "제공된 테스트 결과 데이터가 없습니다.",
	buttons: {
		details: "상세 정보",
		synopsis: "시놉시스",
		storePage: "상점 페이지",
		dlc: "DLC",
		communityHub: "커뮤니티 허브",
		pointsShop: "포인트 상점",
		discussions: "토론방",
		guides: "가이드",
		support: "고객지원",
		install: "설치",
		uninstall: "설치 제거",
		cancel: "취소",
		achievements: "도전 과제"
	},
	labels: {
		store: "스토어:",
		name: "게임명:",
		size: "용량:",
		developer: "개발사:",
		publisher: "배급사:",
		released: "출시일:",
		metacritic: "메타크리틱:",
		status: "상태:",
		spaceRequired: "필요 공간",
		lastPlayed: "최근 플레이",
		playTime: "플레이 시간",
		never: "없음"
	},
	compatibility: {
		modalTitle: "Steam Deck 호환성 정보",
		viewOnProtonDb: "ProtonDB에서 확인하기",
		close: "닫기",
		verified: "완벽 지원",
		playable: "플레이 가능",
		unsupported: "지원 안 함",
		unknown: "정보 없음"
	},
	failedToLoadMetadata: "메타데이터 로드 실패",
	lastSession: "지난 세션: {{names}} 달성"
};
var gameDetailsSettings$3 = {
	title: "게임 상세 화면 레이아웃",
	simple: "기본형",
	detailed: "상세형"
};
var gogLanguageModal$3 = {
	title: "언어 선택",
	description: "{{title}}에 다운로드하여 적용할 언어를 선택하세요",
	label: "언어",
	install: "설치",
	cancel: "취소"
};
var accountSwitch$3 = {
	title: "계정 변경 감지됨",
	description: "다른 Steam 계정으로 로그인했습니다. Unifideck 데이터를 어떻게 처리하시겠습니까?",
	migrate: "Unifideck 데이터 마이그레이션",
	migrateDescription: "이전 계정의 게임 및 라이브러리 이미지 복사",
	freshStart: "새로 시작",
	freshStartDescription: "다른 Epic/GOG/Amazon/Ubisoft 계정을 연결할 수 있도록 저장된 연동 세션 정보 삭제",
	skip: "건너뛰기",
	toastMigrateTitle: "마이그레이션 완료",
	toastMigrateBody: "바로가기 {{shortcuts}}개, 아트워크 {{artwork}}개가 이동되었습니다",
	toastClearTitle: "인증 토큰이 삭제됨",
	toastClearBody: "상점 연동 로그인 정보가 리셋되었습니다. 이제 다른 계정으로 로그인할 수 있습니다."
};
var ubisoftAuth$3 = {
	title: "Ubisoft Connect 로그인",
	description: "라이브러리를 연동하려면 Ubisoft Connect를 통해 로그인해 주세요.",
	signInViaUpc: "Ubisoft Connect로 로그인",
	upcDescription: "Ubisoft Connect 로그인 창이 열립니다. 로그인 완료 시 세션 정보가 자동으로 등록됩니다.",
	launchingUpc: "Ubisoft Connect 실행 중...",
	waitingForLogin: "로그인 대기 중...",
	retryLaunch: "다시 시도",
	cancel: "취소",
	launchFailed: "Ubisoft Connect를 실행할 수 없습니다. 다시 시도해 주세요.",
	signingIn: "로그인 중...",
	emailLabel: "이메일",
	passwordLabel: "비밀번호",
	signIn: "로그인",
	twoFactorTitle: "2단계 인증",
	twoFactorDescription: "인증 앱에 표시되는 보안 코드를 입력해 주세요.",
	twoFactorLabel: "보안 코드",
	verify: "인증",
	verifying: "인증 중...",
	errorEmptyFields: "이메일과 비밀번호를 입력해 주세요",
	errorEmpty2FA: "보안 코드를 입력해 주세요",
	errorLoginFailed: "로그인 실패. 정보를 다시 확인해 주세요.",
	errorInvalid2FA: "잘못된 보안 코드입니다. 다시 시도해 주세요.",
	showPassword: "비밀번호 표시",
	hidePassword: "비밀번호 숨기기",
	doubleLoginWarning: "Ubisoft 로그인이 2회 필요할 수 있습니다."
};
var relativeDate$3 = {
	today: "오늘",
	yesterday: "어제"
};
var common$3 = {
	thisGame: "이 게임을",
	game: "게임",
	unknown: "알 수 없음",
	ok: "확인",
	cancel: "취소",
	working: "처리 중…",
	loading: "불러오는 중…",
	free: "무료",
	close: "닫기"
};
var info$3 = {
	expandDetails: "자세히 보기",
	collapseDetails: "접기"
};
var microsoft$3 = {
	notCompatible: "이 게임 형식은 SteamOS와 호환되지 않습니다",
	playOnCloud: "클라우드에서 플레이",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "Microsoft / Xbox 계정으로 로그인하세요",
	accountConnected: "Microsoft 계정 연동됨",
	loggedOut: "Microsoft Store 로그아웃됨",
	xcloudNotInstalled: "xCloud 게임은 기기에 로컬로 설치되지 않습니다",
	chromiumRequired: "Microsoft Edge 필요",
	chromiumRequiredMessage: "로그인을 완료하려면 Microsoft Edge 브라우저가 필요합니다. 자동으로 설치할까요? (최초 1회만 필요합니다).",
	chromiumInstallButton: "Microsoft Edge 설치",
	chromiumInstalling: "Microsoft Edge 설치 중...",
	chromiumInstalled: "Microsoft Edge 설치 완료!",
	chromiumAlreadyInstalled: "호환되는 브라우저가 이미 설치되어 있습니다",
	chromiumInstallFailed: "Microsoft Edge를 설치하지 못했습니다. 수동으로 설치해 주세요: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "Microsoft Edge 설치 시간이 초과되었습니다. 인터넷 연결을 확인하세요.",
	browserAlreadyInstalled: "호환되는 브라우저가 이미 설치되어 있습니다",
	browserInstalled: "Microsoft Edge가 성공적으로 설치되었습니다!",
	browserInstallFailed: "Microsoft Edge를 설치하지 못했습니다. 수동으로 설치해 주세요: flatpak install com.microsoft.Edge",
	flatpakNotFound: "이 시스템에서 Flatpak을 이용할 수 없습니다",
	signInRequired: "플레이하려면 로그인 필요",
	syncSkippedNoSubscription: "유효한 Xbox Game Pass 멤버십을 찾을 수 없습니다. xCloud 라이브러리 동기화가 취소되었습니다.",
	syncSkippedTierUnknown: "Xbox Game Pass 구독은 확인되었으나 요금제 정보를 가져올 수 없습니다. 이용할 수 없는 게임이 표시되지 않도록 동기화를 건너뛰었습니다.",
	subscriptionCheckFailed: "Xbox Game Pass 멤버십을 확인할 수 없습니다. xCloud 동기화를 건너뛰었습니다. 연결이 원활할 때 다시 시도해 주세요.",
	subscriptionDetected: "유효한 Xbox Game Pass {{tier}} 구독을 확인했습니다."
};
var playButton$3 = {
	play: "플레이",
	install: "설치",
	cancel: "취소",
	retry: "다시 시도",
	downloadFailed: "다운로드 실패",
	installing: "설치 중",
	controllerConfig: "컨트롤러 설정",
	appSettings: "앱 설정"
};
var play$3 = {
	play: "플레이",
	resume: "재개",
	stop: "중지",
	cancel: "취소",
	cancelling: "취소 중…",
	update: "업데이트",
	uninstall: "제거",
	queued: "대기 중",
	downloading: "다운로드 중",
	extracting: "압축 해제 중",
	verifying: "검증 중",
	finalizing: "설치 마무리 중",
	cancelConfirmTitle: "다운로드를 취소할까요?",
	cancelConfirmBody: "{{title}} 다운로드를 취소하시겠습니까? 지금까지 다운로드된 파일은 삭제됩니다.",
	cancelConfirmConfirm: "다운로드 취소",
	cancelConfirmCancel: "다운로드 계속하기",
	playOnCloud: "클라우드에서 플레이",
	cloudSave: {
		autoPullOn: "실행 시 클라우드 저장 데이터가 자동으로 다운로드됩니다.",
		close: "닫기",
		download: "클라우드 저장 데이터 다운로드",
		filesCount: "{{count}}개 파일",
		label: "클라우드 저장 데이터",
		menuTitle: "클라우드 저장 데이터 — {{game}}",
		noCloudData: "알 수 없음",
		noCloudSupport: "클라우드 저장 미지원",
		setLocation: "저장 위치 설정",
		setLocationHint: "이 게임의 저장 데이터가 보관되는 폴더를 선택하십시오.",
		setLocationTitle: "이 게임의 저장 폴더 선택",
		statusCloudAvailable: "클라우드 저장 데이터 사용 가능",
		statusNoSaves: "로컬 저장 데이터 없음",
		statusSyncing: "클라우드 저장 데이터 동기화 중…",
		statusUnresolved: "저장 위치를 찾을 수 없음 — 이 게임은 동기화가 작동하지 않을 수 있습니다.",
		unresolvedHint: "저장 폴더를 감지할 수 있도록 게임을 한 번 실행하거나, 수동으로 위치를 설정하십시오.",
		upload: "로컬 저장 데이터 업로드"
	},
	exe: {
		browse: "찾아보기…",
		changeFailed: "실행 파일을 변경할 수 없습니다",
		changed: "실행 파일 변경됨",
		defaultTag: "(기본값)",
		menuItem: "실행 파일 변경…",
		reset: "기본 실행 파일로 재설정",
		resetButton: "기본값으로 재설정",
		subtitle: "이 게임이 실행할 파일을 선택하십시오 (예: 런처를 건너뛰고 게임을 직접 실행). 작업 디렉토리와 클라우드 저장 데이터는 영향을 받지 않습니다.",
		title: "실행 파일 변경 — {{game}}",
		unavailable: "이 게임의 폴더를 읽을 수 없습니다."
	}
};
var playMeta$3 = {
	spaceRequired: "필요한 용량",
	installedSize: "설치 크기",
	lastPlayed: "최근 플레이",
	neverPlayed: "플레이한 적 없음",
	hoursShort: "시간",
	minutesShort: "분",
	played: "플레이함"
};
var downloads$3 = {
	title: "다운로드",
	empty: "활성화된 다운로드가 없습니다. 라이브러리에서 게임을 설치하면 여기에 표시됩니다.",
	current: "현재 다운로드 중",
	queued: "대기 목록",
	finished: "최근 완료 항목",
	cancel: "취소",
	removeFromQueue: "삭제",
	play: "플레이",
	outcome: {
		queued: "대기 중",
		downloading: "다운로드 중",
		completed: "완료됨",
		cancelled: "취소됨",
		error: "에러"
	}
};
var navigation$3 = {
	loading: "불러오는 중...",
	storesTitle: "상점 연동",
	syncTitle: "라이브러리 동기화",
	languageTitle: "언어"
};
var security$3 = {
	panelTitle: "보안",
	counters: {
		label: "카운터",
		empty: "시작 이후 보고된 이벤트가 없습니다"
	},
	log: {
		label: "보안 로그",
		entryCount: "{{count}}개 항목",
		entryCount_other: "{{count}}개 항목",
		emptyFiltered: "필터에 일치하는 이벤트 로그가 없습니다"
	},
	filter: {
		label: "필터",
		all: "모든 이벤트"
	},
	timeline: {
		label: "24시간 타임라인 (이벤트 {{count}}개)",
		label_other: "24시간 타임라인 (이벤트 {{count}}개)",
		axisStart: "-24시간",
		axisEnd: "현재"
	},
	relativeTime: {
		seconds: "{{count}}초 전",
		minutes: "{{count}}분 전",
		hours: "{{count}}시간 전",
		days: "{{count}}일 전"
	},
	bruteforce: {
		warningTitle: "경고: 비밀번호 대입 공격 감지 (경고 단계)",
		escalationTitle: "알람: 비밀번호 대입 공격 감지 (위험 단계)",
		detail: "최근 {{window}}초 동안 {{failures}}회의 복호화 시도 실패가 확인되었습니다. 임계값: {{warning}} (경고) / {{escalation}} (위험).",
		resetButton: "확인 및 감지기 초기화"
	},
	actions: {
		clearLog: "보안 로그 및 카운터 삭제"
	},
	events: {
		tokenEncrypted: "토큰 암호화됨",
		tokenDecrypted: "토큰 복호화됨",
		decryptFailed: "복호화 실패",
		tokenFileMigrated: "토큰 파일 이전됨",
		legacyPlaintextDetected: "기존 평문 데이터 감지됨",
		authFlowStarted: "인증 프로세스 시작됨",
		authFlowCompleted: "인증 프로세스 완료됨",
		authFlowFailed: "인증 프로세스 실패함",
		permissionsCheck: "파일 권한 점검 완료",
		permissionsRepaired: "파일 권한 수정 완료",
		bruteforceSuspected: "무차별 대입 공격 의심",
		deviceResetDetected: "기기 초기화 감지됨",
		fingerprintInitialized: "핑거프린트 초기화 완료",
		externalAuthCheckFailed: "외부 연동 인증 확인 실패",
		configValidationCompleted: "환경설정 유효성 점검 완료",
		configValidationFailed: "환경설정 유효성 점검 실패"
	},
	configValidation: {
		degradedTitle: "잘못된 환경설정: 제한 모드로 작동 중",
		detail: "시작 시 {{count}}개의 설정 오류가 확인되었습니다",
		detail_other: "시작 시 {{count}}개의 설정 오류가 확인되었습니다",
		sourceDefaults: "기본값",
		sourceUser: "사용자 설정",
		sourceMerged: "병합됨",
		overflow: "+ 외 {{count}}개 에러",
		overflow_other: "+ 외 {{count}}개 에러"
	}
};
var cloudSyncBehavior$3 = {
	_comment: "각 상점별로 클라우드 세이브 동기화 에러를 표시할 방식을 설정하는 패널입니다. 5개의 store_label 항목은 CloudSyncBehaviorPanel.tsx 에 렌더링되는 행을 담당합니다. Microsoft Store는 xCloud 스트리밍이므로 로컬 저장 데이터가 없어 의도적으로 제외되었습니다.",
	title: "클라우드 세이브 동기화 설정",
	description: "게임 실행 시 클라우드 저장 데이터를 가져오거나 업로드하는 과정에 실패했을 때 Unifideck의 대응 방식을 선택하세요.",
	loadError: "현재 설정 정보를 가져올 수 없습니다",
	saveError: "설정을 저장하지 못했습니다. 다시 시도해 주세요",
	store_label: {
		"default": "기본 설정 (모든 상점)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "알림 없음 (로그만 기록)",
		toast: "알림 표시 (팝업 알림)"
	}
};
var cloudSync$3 = {
	_comment: "클라우드 동기화 실패 시 런처에서 보내는 팝업 알림입니다. 에러 내용은 백엔드 데이터의 error_i18n_key 값을 바탕으로 cloudSync.error.<code> 양식으로 매핑됩니다.",
	error: {
		network_unreachable: "네트워크 연결 불가",
		auth_expired: "연동 로그인 만료됨",
		forbidden: "접근 권한 없음",
		quota_exceeded: "클라우드 용량 초과",
		server_error: "서버 응답 없음",
		timed_out: "요청 시간 초과",
		disk_full: "디스크 용량 부족",
		disk_space_low: "건너뜀: 디스크 여유 공간 부족 (공간 확보 후 다시 시도해 주세요)",
		permission_denied: "액세스 거부됨",
		cancelled: "취소됨",
		unknown: "예기치 못한 에러 발생"
	},
	unavailableNativeDep: "이 시스템에서는 클라우드 저장이 비활성화되어 있습니다 — Python ({{python}})에 필요한 구성 요소가 없습니다. 로컬 저장 데이터는 영향을 받지 않으며, 동기화를 복원하려면 시스템 업데이트 후 플러그인을 재빌드/재설치하십시오."
};
var launchLogs$3 = {
	_comment: "LaunchLogsModal용 다국어 텍스트 정보입니다. 런처 실패 알림(서킷 브레이커 + LauncherError)에 생성된 unifideck://show-logs/<launch_id> 링크 클릭 시 창이 열립니다. ~/.local/share/unifideck/launches/<launch_id>.log 에 기록된 로그 파일을 읽어와 중요도에 따라 색상별로 보여 줍니다.",
	title: "실행 로그 — {{launchId}}",
	close: "닫기",
	copyAll: "전체 복사",
	"export": "파일로 내보내기",
	copied: "클립보드에 복사 완료",
	copyFailed: "복사 실패",
	exporting: "내보내는 중…",
	exportedTo: "{{path}} 경로로 내보냈습니다",
	exportFailed: "내보내기 실패: {{error}}",
	loading: "로그 로드 중…",
	notAvailable: "해당 실행 로그가 존재하지 않거나 보존 기간(7일)이 만료되었습니다.",
	error: "로그 파일을 읽을 수 없습니다: {{error}}",
	fileInfo: "{{path}} — 전체 {{total}}행 중 {{shown}}행 표시 중",
	filterLabel: "표시 대상",
	filterInfo: "정보",
	filterWarning: "경고",
	filterError: "에러",
	nextError: "다음 에러 ({{current}}/{{total}})",
	noErrors: "오류 없음",
	allFiltered: "현재 필터 설정에 의해 모든 로그행이 숨김 처리되었습니다. 내용을 확인하려면 최소 하나의 필터 중요도를 켜 주세요."
};
var settings$3 = {
	_comment: "특정 설정 요소(LanguageSelector, CloudSyncBehaviorPanel) 외에 UnifideckSettingsPanel에서 사용되는 전반적인 텍스트 정보입니다. refreshAll 버튼은 연동되어 있는 모든 상점의 sync_all()을 백그라운드로 작동시킵니다. 라이브러리 목록 탭에서는 SYNC_PROGRESS 이벤트 통신을 받아 동기화 진행 상황을 표시해 줍니다.",
	storeConnections: "상점 연동 관리",
	librarySync: "라이브러리 동기화",
	language: "언어",
	languageAuto: "시스템 언어 자동 감지",
	refreshAll: {
		sectionTitle: "라이브러리 작업",
		button: "모든 라이브러리 동기화",
		buttonBusy: "실행 중…",
		startedTitle: "라이브러리 동기화 시작됨",
		startedBody: "모든 상점의 동기화가 백그라운드에서 진행됩니다.",
		startedSingle: "{{store}} 라이브러리 동기화가 백그라운드에서 실행 중입니다.",
		failedTitle: "라이브러리 동기화 시작 실패"
	}
};
var saveFolder$3 = {
	_comment: "SaveFolderModal용 다국어 텍스트 정보입니다. 클라우드 세이브 동기화 에러 시 로컬 저장 데이터를 확인하고 싶을 때 unifideck://open-save-folder/<store>/<game_id> 바로가기 링크를 통해 작동합니다. count 및 size 정보는 자동으로 계산되어 채워지며, truncated 문구는 파일이 너무 많아 일부 표시를 제한했을 때에만 화면에 보여집니다.",
	title: "세이브 폴더 — {{store}} / {{gameId}}",
	close: "닫기",
	copyPath: "경로 복사",
	copyFileList: "파일 목록 복사",
	depthLabel: "하위 깊이",
	filterLabel: "필터",
	loading: "폴더 내용 확인 중…",
	noSavesYet: "해당 게임의 로컬 세이브 데이터를 찾을 수 없습니다. 다음 클라우드 동기화 완료 시 폴더가 생성됩니다.",
	aggregate: "파일 {{count}}개, 전체 크기 {{size}}",
	truncated: "그 외 {{count}}개의 파일 (크기 {{size}})이 생략되었습니다. 필요 시 필터 조건을 변경해 주세요.",
	error: "세이브 폴더를 읽어오지 못했습니다: {{error}}"
};
var library$3 = {
	_comment: "실행 실패 반복 시 자동 실행 일시 중단 및 강제 실행/오류 초기화 조작 요소를 포함하는 게임별 라이브러리 뷰 설정입니다.",
	circuitBreaker: {
		_comment: "오류 누적 차단 장치(서킷 브레이커) UI 설정입니다. 지정된 시간 내에 발생한 오류 횟수가 설정값(기본 600초 내 3회)에 다다를 때 작동합니다. 강제 실행 시 1회에 한해 차단기를 통과하며, 초기화 선택 시 기록된 실패 횟수가 리셋됩니다.",
		badgeRecent: "최근 실행 실패 {{count}}회",
		badgeRecent_other: "최근 실행 실패 {{count}}회",
		badgeTooltip: "최근 {{count}}회 실행에 실패하여 자동 실행 차단 장치가 작동되었습니다.",
		forceLaunchButton: "강제 실행",
		resetButton: "기록 초기화",
		failureKind: {
			fastBoot: "실행 즉시 강제 종료",
			launcherError: "런처 준비 단계 오류"
		}
	}
};
var pickStorage$3 = {
	title: "설치 폴더 설정",
	customOption: "다른 폴더 선택...",
	customLabel: "사용자 지정 폴더",
	change: "변경...",
	gameSize: "설치에 필요한 공간"
};
var restart$3 = {
	title: "동기화 완료!",
	body: "새로운 게임과 이미지를 적용하려면 Steam을 재시작해 주세요",
	confirm: "지금 재시작",
	later: "나중에"
};
var updater$3 = {
	checkButton: "업데이트 확인",
	checkCompleteMessage: "최신 버전 정보가 정상적으로 로드되었습니다.",
	checkCompleteTitle: "업데이트 확인 완료",
	checkFailedTitle: "확인 실패",
	checkingButton: "확인 중...",
	currentTitle: "현재 버전",
	downgradeButton: "v{{version}}(으)로 다운그레이드",
	installFailedTitle: "설치 실패",
	installTimeoutMessage: "Decky Loader에서 응답이 없습니다. 설치가 멈췄거나 실패했을 수 있습니다. 다시 시도해 주세요.",
	installedLabel: "설치됨",
	installingButton: "설치 중...",
	installingTitle: "플러그인 설치 중",
	latestLabel: "최신 버전",
	modalTitle: "UNIFIDECK v{{version}} — 릴리스 노트",
	noBackend: "Decky 백엔드 연결 불가.",
	noReleaseNotes: "릴리스 노트가 존재하지 않습니다.",
	reinstallButton: "v{{version}} 재설치",
	refreshFailedMessage: "최신 버전 정보를 확인할 수 없습니다. 연결 상태를 확인하고 다시 시도해 주세요.",
	releaseGoneMessage: "이 릴리스는 더 이상 제공되지 않습니다. 업데이트를 확인한 후 다시 선택해 주세요.",
	releaseNotesButton: "릴리스 노트",
	stageDownload: "다운로드 중…",
	stageFinish: "완료 중…",
	stageInstall: "설치 중…",
	stageOpen: "패키지 읽는 중…",
	stageParse: "무결성 확인 중…",
	stageRemove: "이전 버전 제거 중…",
	stageStart: "시작 중…",
	stageWorking: "설치 중…",
	titleLoading: "버전 검사 중",
	typeDowngrade: "다운그레이드 진행:",
	typeReinstall: "재설치 진행",
	typeUpdate: "업데이트 진행:",
	updateButton: "v{{version}}(으)로 업데이트"
};
var achievements$3 = {
	unlockedTitle: "도전 과제 달성",
	unlockedBody: "{{name}}"
};
var achievementsModal$3 = {
	title: "도전 과제",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "새로 고침",
	close: "닫기",
	loading: "도전 과제 불러오는 중…",
	empty: "이 게임에는 도전 과제가 없습니다.",
	hidden: "숨겨진 도전 과제",
	unlockedOn: "{{date}}에 달성",
	error: "도전 과제를 불러오지 못했습니다.",
	errorOffline: "도전 과제를 보려면 인터넷에 연결하세요.",
	errorAuth: "도전 과제를 보려면 이 게임의 스토어에 로그인하세요.",
	errorUnavailable: "이 게임에서는 도전 과제를 사용할 수 없습니다."
};
var collectionSettings$3 = {
	title: "STEAM 컬렉션",
	enabled: "컬렉션 활성화됨",
	disabled: "컬렉션 비활성화됨"
};
var koKR = {
	_comment: _comment$3,
	tabs: tabs$3,
	auth: auth$3,
	storage: storage$3,
	storageSettings: storageSettings$3,
	gameGrid: gameGrid$3,
	downloadsTab: downloadsTab$3,
	cloudSave: cloudSave$3,
	storeConnections: storeConnections$3,
	librarySync: librarySync$3,
	cleanup: cleanup$3,
	captureLogs: captureLogs$3,
	installButton: installButton$3,
	confirmModals: confirmModals$3,
	toasts: toasts$3,
	uninstallModal: uninstallModal$3,
	languageSettings: languageSettings$3,
	errors: errors$3,
	sync: sync$3,
	force_sync: force_sync$3,
	artwork: artwork$3,
	deckTabs: deckTabs$3,
	unifiedLibrary: unifiedLibrary$3,
	gameInfoPanel: gameInfoPanel$3,
	gameDetailsSettings: gameDetailsSettings$3,
	gogLanguageModal: gogLanguageModal$3,
	accountSwitch: accountSwitch$3,
	ubisoftAuth: ubisoftAuth$3,
	relativeDate: relativeDate$3,
	common: common$3,
	info: info$3,
	microsoft: microsoft$3,
	playButton: playButton$3,
	play: play$3,
	playMeta: playMeta$3,
	downloads: downloads$3,
	navigation: navigation$3,
	security: security$3,
	cloudSyncBehavior: cloudSyncBehavior$3,
	cloudSync: cloudSync$3,
	launchLogs: launchLogs$3,
	settings: settings$3,
	saveFolder: saveFolder$3,
	library: library$3,
	pickStorage: pickStorage$3,
	restart: restart$3,
	updater: updater$3,
	achievements: achievements$3,
	achievementsModal: achievementsModal$3,
	collectionSettings: collectionSettings$3
};

var _comment$2 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$2 = {
	settings: "设置",
	downloads: "下载"
};
var auth$2 = {
	connect: "绑定",
	disconnect: "断开",
	status: {
		connected: "已连接",
		disconnected: "未连接",
		checking: "正在检查…",
		error: "错误 — 请检查日志"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$2 = {
	title: "存储空间",
	defaultUpdated: "默认存储位置已更新",
	isDefault: "默认",
	setDefault: "设为默认",
	customPath: "自定义路径",
	up: "↑ 返回上级",
	refresh: "刷新",
	useThisPath: "使用此路径"
};
var storageSettings$2 = {
	title: "下载设置",
	installLocationLabel: "安装位置",
	installLocationDescription: "新游戏将被下载到的位置",
	pathLabel: "路径",
	loading: "正在加载存储选项...",
	toastUpdatedTitle: "存储位置已更新",
	toastUpdatedBody: "新游戏将安装到 {{location}}",
	toastFailedTitle: "更新失败",
	toastFailedBody: "{{error}}",
	unknown: "未知",
	browseButton: "浏览...",
	clearCustom: "清除自定义位置",
	customPathSet: "自定义安装位置已设置",
	newFolder: "新建文件夹",
	newFolderCreate: "创建",
	selectFolder: "使用此文件夹",
	emptyDirectory: "空文件夹",
	showHidden: "显示隐藏文件",
	selectDevice: "选择存储设备",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "修改时间 (最新)",
	sortModifiedOldest: "修改时间 (最旧)",
	sortCreatedNewest: "创建时间 (最新)",
	sortCreatedOldest: "创建时间 (最旧)",
	sortSizeLargest: "大小 (最大)",
	sortSizeSmallest: "大小 (最小)",
	"validationErrors.notAbsolute": "路径必须是绝对路径",
	"validationErrors.systemDir": "无法使用系统目录",
	"validationErrors.notWritable": "该位置不可写",
	"validationErrors.notFound": "目录不存在",
	"validationErrors.homeRoot": "无法使用用户根目录",
	freeSpaceLabel: "{{label}} (剩余 {{freeSpace}} GB)"
};
var gameGrid$2 = {
	loading: "正在加载游戏...",
	noGamesFound: "未找到游戏",
	noGamesHint: "请尝试在 Unifideck 设置中同步您的游戏库",
	showingGames: "正在显示 {{count}} 款游戏",
	installed: "已安装",
	notInstalled: "未安装",
	playtime: "已玩 {{hours}} 小时 {{minutes}} 分钟"
};
var downloadsTab$2 = {
	currentDownload: "当前下载",
	queuedDownloads: "队列中 ({{count}})",
	recentlyCompleted: "最近完成",
	noActiveDownloads: "没有正在进行的下载",
	noDownloads: "没有下载任务。从您的库中安装游戏即可在此处查看。",
	loadingLabel: "正在加载...",
	loadingMessage: "正在获取下载队列...",
	confirmCancelTitle: "确认取消",
	confirmCancelDescription: "您确定要取消下载 {{game}} 吗？",
	confirmCancelYes: "是",
	confirmCancelNo: "否",
	cancelDownload: "取消",
	toastDownloadCancelledTitle: "下载已取消",
	toastDownloadCancelledBody: "下载任务已从队列中移除。",
	toastCancelFailedTitle: "取消失败",
	toastCancelFailedBody: "无法取消下载: {{error}}",
	status: {
		queued: "队列中",
		downloading: "下载中",
		completed: "完成",
		cancelled: "已取消",
		error: "错误"
	},
	preparingDownload: "正在准备下载...",
	phaseExtracting: "正在解压 {{game}}...",
	phaseVerifying: "正在验证 {{game}}...",
	internalStorage: "内部存储 (剩余 {{freeSpace}} GB)",
	sdCard: "SD 卡 (剩余 {{freeSpace}} GB)",
	customLocation: "自定义: {{path}} (剩余 {{freeSpace}} GB)",
	extractingLabel: "正在解压...",
	verifyingLabel: "正在验证...",
	updateQueuedLabel: "更新排队中",
	downloadQueuedLabel: "下载排队中",
	downloadingUpdateLabel: "正在下载更新",
	downloadingLabel: "正在下载...",
	finalizingInstallation: "正在完成安装...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "剩余时间: {{eta}}",
	preparingMessage: "正在初始化容器并同步云端存档...",
	preparingLabel: "正在设置游戏...",
	installingViaUpcLabel: "正在通过 Ubisoft Connect 安装",
	extractingMessage: "正在解压文件...",
	verifyingMessage: "正在校验文件... {{pct}}%",
	installingViaUpcMessage: "请按照 Ubisoft Connect 窗口的提示完成安装..."
};
var cloudSave$2 = {
	title: "云端存档冲突",
	description: "您的本地数据与 {{game}} 的云端存档不同",
	local: "本地:",
	cloud: "云端:",
	newer: "(较新)",
	useCloud: "使用云端存档",
	useLocal: "使用本地存档",
	unknownDate: "未知",
	toastUsingCloudTitle: "使用云端存档中",
	toastUsingCloudBody: "正在下载 {{game}} 的云端存档",
	toastUsingLocalTitle: "使用本地存档中",
	toastUsingLocalBody: "正在上传 {{game}} 的本地存档",
	toastErrorTitle: "错误",
	toastErrorBody: "无法解决存档冲突",
	filesSize: "{{count}} 个文件 · {{size}}",
	uploadBlockedEmpty: "已跳过 {{game}} 的云上传 — 未找到本地存档",
	uploadSkippedTitle: "已跳过云上传",
	uploadSkippedBody: "{{game}} 暂无可上传的本地存档。"
};
var storeConnections$2 = {
	title: "商店连接",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "已连接",
	checking: "正在检查...",
	authenticate: "登录",
	legendaryNotInstalled: "⚠️ 正在安装 Epic Games 客户端...",
	nileNotInstalled: "⚠️ 未找到 Amazon Games 客户端",
	error: "错误 — 请检查日志"
};
var librarySync$2 = {
	title: "游戏库同步",
	syncLibraries: "同步游戏库",
	forceSync: "强制同步",
	syncing: "正在同步...",
	cancelSync: "取消同步",
	retryStatusCheck: "重试状态检查",
	gamesSynced: "已同步游戏 {{synced}} / {{total}}",
	artworkDownloaded: "已下载封面 {{synced}} / {{total}}",
	metadataExtracted: "已提取元数据 {{synced}} / {{total}}",
	steamMetadataDownloaded: "Steam 元数据: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDB 元数据: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacritic 数据: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWG 元数据: {{synced}} / {{total}}",
	compatChecked: "兼容性数据: {{synced}} / {{total}}",
	cancelling: "正在取消…",
	error: "错误"
};
var cleanup$2 = {
	title: "清理",
	deleteAll: "删除所有 UNIFIDECK 库和缓存",
	modalTitle: "删除所有 Unifideck 数据？",
	modalDescription: "这将删除所有的 Unifideck 快捷方式、Steam 封面图、保存的商店登录状态和缓存数据。已安装的游戏仍将保留在磁盘上，可以重新同步。激活下方的破坏性模式可同时删除已安装的游戏文件。",
	deleteFilesLabel: "同时删除已安装的游戏文件 (破坏性)",
	deleteFilesDescription: "删除安全根目录下每个 Unifideck 游戏在磁盘上的安装目录。",
	confirmDelete: "是的，删除全部",
	cancel: "取消",
	deleting: "正在删除..."
};
var captureLogs$2 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (zh-CN).",
	title: "收集日志",
	description: "将所有 Unifideck 日志收集到下载文件夹中的一个 zip 文件里，便于附加到错误报告中。",
	capture: "收集日志",
	capturing: "正在收集日志…",
	savedTo: "已保存到你的下载文件夹",
	bundleInfo: "{{files}} 个文件 · {{size}}",
	partialNote: "有 {{count}} 个文件无法读取。该压缩包仍然可用。",
	partialNote_other: "有 {{count}} 个文件无法读取。该压缩包仍然可用。",
	copyPath: "复制路径",
	modalTitle: "创建诊断压缩包？",
	modalDescription: "Unifideck 会将日志收集到下载文件夹中的一个 zip 文件里，便于你附加到错误报告中。",
	modalIncludes: "包含：插件与游戏启动日志、同步和安装历史、你的游戏名称与安装路径，以及设备信息（型号、操作系统和存储）。",
	modalExcludes: "绝不包含：你的密码、商店登录令牌或浏览器 Cookie。",
	modalConfirm: "创建压缩包",
	cancel: "取消"
};
var installButton$2 = {
	play: "运行",
	resume: "继续",
	install: "安装 {{title}}",
	installNative: "安装",
	uninstall: "卸载 {{title}}",
	cancel: "✖ 取消",
	processing: "正在处理...",
	checking: "正在检查...",
	size: "{{size}} GB",
	update: "更新",
	updateAvailable: "有可用更新",
	updateAvailableDescription: "新版本已准备好安装。"
};
var confirmModals$2 = {
	installTitle: "确认安装",
	installDescription: "您确定要安装 {{title}} 吗？",
	uninstallTitle: "确认卸载",
	uninstallDescription: "您确定要卸载 {{title}} 吗？",
	cancelTitle: "确认取消",
	cancelDescription: "您确定要取消下载 {{title}} 吗？",
	forceSyncTitle: "强制同步设置",
	forceSyncDescription: "您想重新同步所有封面吗？这会覆盖您对封面的任何手动更改。选择“保留封面”将仅下载缺失的封面。",
	resyncArtwork: "重新同步封面",
	keepArtwork: "保留封面",
	steamRestartTitle: "同步完成！",
	steamRestartDescription: "请重启 Steam 以应用您的新游戏和封面",
	steamRestartDescriptionStore: "请重启 Steam 以应用您的新游戏和 {{store}} 封面",
	steamRestartCleanupTitle: "清理完成！",
	steamRestartCleanupDescription: "请重启 Steam 以完全移除已删除的游戏和封面",
	restartNow: "立即重启",
	later: "稍后",
	ubisoftInstallTitle: "需要 Ubisoft Connect",
	ubisoftInstallDescription: "这会加载安装 {{title}} 所需的 Ubisoft Connect 客户端。",
	ubisoftInstallConfirm: "打开 Ubisoft Connect",
	yes: "是",
	no: "否",
	amazonUpdateTitle: "需要重新完整下载",
	amazonUpdateDescription: "Amazon 更新需要完全重新下载 {{title}}。这可能需要一些时间。是否继续？",
	updateConfirm: "更新"
};
var toasts$2 = {
	downloadStarted: "下载已开始。请检查 UNIFIDECK > 下载",
	downloadQueued: "下载已排队",
	downloadQueuedBody: "{{title}} 已加入队列。",
	downloadFailedUnknown: "未知错误。",
	updateQueued: "更新已排队",
	uninstallDone: "卸载已完成。",
	ubisoftLauncherOpening: "正在打开 Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "正在为 {{title}} 打开 Ubisoft Connect。请在此处安装游戏，然后返回启动游戏。",
	ubisoftLauncherAlreadyOpenMessage: "此安装已打开 Ubisoft Connect。",
	ubisoftLauncherOpenFailed: "无法打开 Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "无法打开 Ubisoft Connect 以进行安装。",
	multipartDetected: "检测到多部分下载",
	multipartMessage: "请耐心等待完成",
	downloadFailed: "下载失败",
	downloadFailedMessage: "无法启动下载。",
	downloadCancelled: "下载已取消",
	downloadCancelledMessage: "{{title}} 的下载已取消。",
	cancelFailed: "取消失败",
	cancelFailedMessage: "无法取消下载。",
	uninstalling: "正在卸载...",
	uninstallingMessage: "正在卸载 {{title}}...",
	uninstallingMessageProton: "正在卸载 {{title}} 及 Proton 文件...",
	uninstallComplete: "卸载已完成",
	uninstallCompleteMessage: "{{title}} 已删除。",
	uninstallCompleteMessageProton: "{{title}} 已删除 (包括 Proton 文件)。",
	installComplete: "安装已完成！",
	installCompleteMessage: "{{title}} 准备就绪。",
	syncComplete: "同步完成！",
	syncCompleteMessage: "已添加 {{count}} 款游戏。请重启 STEAM (完全退出它，而不仅仅是返回游戏模式) 以在您的库中查看它们。",
	forceSyncComplete: "强制同步已完成！",
	forceSyncCompleteMessage: "已更新 {{count}} 款游戏。请重启 STEAM 以应用更改。",
	syncCancelled: "同步已取消",
	authTimeout: "认证超时",
	authTimeoutMessage: "{{store}} 认证已过期。请重试。",
	authFailed: "认证失败",
	authFailedMessage: "无法启动认证流程",
	authError: "认证错误",
	authConnected: "{{store}} 已连接！",
	authConnectedMessage: "成功通过 {{store}} 认证",
	cleanupStarted: "清理已启动",
	cleanupStartedMessage: "正在删除快捷方式、封面和保存的登录状态。这可能需要一点时间…",
	cleanupSuccessful: "清理成功",
	cleanupSuccessfulMessage: "已删除 {{games}} 个快捷方式，{{artwork}} 个封面文件，已注销 {{stores}} 个商店，已删除 {{files}} 个安装目录。缓存已清空。",
	cleanupNoop: "无可清理内容",
	cleanupNoopMessage: "Unifideck 没有需要删除的快捷方式、封面或保存的登录状态。",
	deleteFailed: "删除失败",
	repairPrefixSuccess: "运行环境修复成功",
	repairPrefixSuccessMessage: "Ubisoft Connect 运行环境已重建",
	repairPrefixFailed: "修复失败",
	repairPrefixFailedMessage: "无法修复运行环境: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "网络错误",
		launchRetry: "正在重试启动",
		retryingUmu: "{{seconds}} 秒后重试 UMU (尝试次数 {{attempt}}/{{max}})...",
		protonUpgrade: "Proton 升级",
		resettingPrefix: "正在为 {{version}} 重置环境...",
		firstTimeSetup: "首次设置",
		initializingPrefix: "正在初始化 Wine 环境...",
		setupCompleteTitle: "设置完成",
		prefixInitialized: "Wine 环境已初始化",
		setupFallback: "设置",
		fallbackInitialization: "备用初始化处理中...",
		launchingGame: "正在启动游戏",
		startingEpicGame: "正在启动 Epic 游戏...",
		startingAmazonGame: "正在启动 Amazon 游戏...",
		startingGogGame: "正在启动 GOG 游戏...",
		startingUbisoftGame: "正在启动 Ubisoft 游戏...",
		dependenciesTitle: "正在安装依赖项",
		installingRedistributables: "正在安装游戏必需的运行库...",
		dependenciesReady: "依赖项就绪",
		redistributablesInstalled: "运行库已安装",
		dependenciesStatus: "依赖项状态",
		checkLogs: "设置完成 (请查看日志以了解详细信息)",
		installingProton: "正在安装 Proton",
		attemptingInstall: "正在尝试安装 {{version}}...",
		epicAuthRequired: "需要 Epic 登录",
		epicAuthRequiredMessage: "请在 Unifideck 设置中登录 Epic Games",
		installingRedist: "正在安装运行库",
		installingRedistMessage: "正在执行 GOG 运行库的首次安装...",
		setupFailed: "设置失败",
		setupFailedMessage: "安装失败，请查看日志了解详细信息",
		prerequisitesTitle: "正在安装预备组件",
		installingPrerequisites: "正在安装必需的游戏组件...",
		prerequisitesReady: "预备组件就绪",
		prerequisitesInstalled: "游戏预备组件已安装",
		prerequisitesFailed: "预备组件警告",
		prerequisitesFailedMessage: "预备组件可能未能正确安装",
		browserRequired: "需要 Microsoft Edge",
		signingIn: "正在登录...",
		cloudSyncDownFailed: "无法为 {{store}} 下载云端存档 ({{error}})。正在以本地存档启动游戏。",
		cloudSyncUpFailed: "无法为 {{store}} 上传云端存档 ({{error}})。您的进度仅保存在本地。",
		errorCircuitBreakerOpen: "{{game_key}} 最近启动失败了 {{count}} 次。已暂停自动运行。",
		launcherError: "无法启动 {{game_key}} ({{error_code}})。请查看日志了解详细信息。",
		downloadingProton: "正在下载 Proton — 这可能需要几分钟...",
		downloadingRuntime: "正在下载 Steam Runtime — 首次启动可能需要几分钟...",
		installingUbisoft: "正在通过 Ubisoft Connect 安装",
		installingUbisoftMessage: "正在打开 Ubisoft Connect — 请在启动器窗口中开始安装。",
		protonReadyBody: "{{version}} 已可使用",
		protonReadyTitle: "Proton 已就绪",
		protonSwitchedTo: "现在使用 {{version}}",
		signingInUbisoft: "正在登录 Ubisoft Connect",
		signingInUbisoftMessage: "正在打开 Ubisoft Connect — 请在那里登录后返回。",
		ubisoftPrefixNotReady: "Ubisoft Connect 尚未就绪",
		ubisoftPrefixNotReadyMessage: "无法为此游戏准备 Ubisoft Connect 前缀。请重试安装；如果仍然失败，请重新运行库同步。",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect 尚未就绪",
		ubisoftAuthPrefixNotReadyMessage: "无法为登录准备 Ubisoft Connect。请稍后重试。",
		ubisoftLaunchIdMissing: "正在打开 Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "无法解析此游戏的启动 ID — 正在打开 Ubisoft Connect，以便您从库中启动它。请重新运行库同步以修复直接启动。"
	},
	updateComplete: "更新已完成！",
	updateCompleteMessage: "{{title}} 已更新。",
	updateCancelled: "更新已取消",
	updateCancelledMessage: "{{title}} 的更新已取消。",
	updateFailed: "更新失败",
	updateFailedMessage: "无法启动更新流程。",
	storeError: "无法与商店 {{store}} 通信 ({{errType}})",
	captureLogsStarted: "正在收集日志…",
	captureLogsStartedMessage: "正在下载文件夹中创建诊断压缩包，需要几秒钟。",
	captureLogsDone: "日志已收集",
	captureLogsDoneMessage: "已保存到 {{path}}（{{size}}，{{files}} 个文件）。",
	captureLogsPartial: "日志已收集（部分）",
	captureLogsPartialMessage: "已保存到 {{path}}（{{size}}，{{files}} 个文件）。有 {{failures}} 个文件无法读取。",
	captureLogsFailed: "无法收集日志",
	captureLogsBusy: "日志收集已在进行中",
	captureLogsCopied: "路径已复制",
	captureLogsCopyFailed: "无法复制路径",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "释放空间",
		signInToStore: "登录商店",
		retrySync: "重试",
		retryLibrarySync: "刷新游戏库",
		openSaveFolder: "查看存档文件夹",
		showLogs: "查看日志"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "无法刷新 {{store}} 游戏库 ({{error}})。您现有的游戏库列表保持可见。"
	},
	cloudPullStarted: "正在下载云端存档…",
	cloudPushStarted: "正在上传本地存档…",
	cloudSavePathFailed: "无法设置存档位置",
	cloudSavePathSet: "存档位置已更新",
	cloudPullDone: "已下载云端存档",
	cloudPullFailed: "下载云端存档失败",
	cloudPushDone: "已上传本地存档",
	cloudPushFailed: "上传本地存档失败"
};
var uninstallModal$2 = {
	title: "确认卸载",
	description: "您确定要卸载 {{title}} 吗？",
	deleteProtonLabel: "同时删除 Proton 文件",
	deleteProtonDescription: "删除兼容性数据和用户文件",
	warningTitle: "警告:",
	warningBody: "这将永久删除保存在 Wine 环境下的存档数据。请确保您有备份。",
	cancel: "取消",
	uninstall: "卸载"
};
var languageSettings$2 = {
	title: "语言",
	label: "显示语言",
	description: "界面以及游戏下载所用语言 (如支持)",
	autoDetect: "自动 (系统)"
};
var errors$2 = {
	legendaryNotFound: "未找到 Epic Games 客户端 (Legendary)",
	gogdlNotFound: "未找到 GOG 客户端 (gogdl)",
	nileNotFound: "未找到 Amazon Games 客户端 (nile)",
	authUrlFailed: "无法获取认证 URL",
	authFailed: "认证失败",
	notAuthenticated: "未认证。请先登录。",
	syncInProgress: "同步已在进行中",
	syncCancelled: "用户取消了同步",
	gameNotFound: "未找到游戏",
	gameNotInstalled: "游戏未安装",
	installFailed: "安装失败 — 请查看日志了解详细信息",
	installNoDirectory: "安装已完成，但无法定位游戏目录",
	uninstallFailed: "卸载失败",
	lockConflict: "锁定冲突 — 请重试",
	unknownStore: "未知商店",
	shortcutWriteFailed: "写入快捷方式失败",
	backgroundSyncDisabled: "后台同步已禁用",
	steamGridDbUnavailable: "SteamGridDB 库不可用",
	deleteSyncInProgress: "正在同步时无法进行删除操作",
	alreadyInQueue: "已在队列中",
	download: {
		diskSpace: "可用磁盘空间不足",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "网络错误 — 请检查您的连接",
		authExpired: "登录已过期 — 请重新登录",
		toolMissing: "未找到所需的下载工具",
		processFailed: "下载失败 — 请重试",
		directoryNotFound: "无法找到已安装的文件",
		lockConflict: "另一项操作正在进行中 — 请重试",
		ubisoftClient: "Ubisoft Connect 客户端错误 — 请尝试修复环境",
		ubisoftClientNotRunning: "Ubisoft Connect 客户端无响应",
		ubisoftBootstrap: "无法配置 Ubisoft Connect 运行环境",
		ubisoftUmuMissing: "未找到 umu-run — 运行 Ubisoft 游戏所必需",
		generic: "安装失败 — 请查看日志了解详细信息"
	},
	unknown: "未知错误",
	steamAppsUnavailable: "SteamClient.Apps API 不可用"
};
var sync$2 = {
	start: "同步游戏库",
	force: "强制同步",
	syncing: "正在同步…",
	cancel: "取消同步",
	cancelling: "正在取消…",
	preparing: "正在准备…",
	downloadingArtwork: "正在为 {{game}} 下载封面图...",
	fetchingGameLists: "正在获取游戏列表...",
	fetchingStore: "正在获取 {{store}} 游戏 ({{current}}/{{total}})...",
	cancelledByUser: "用户取消了同步",
	checkingInstalledGames: "正在检查已安装的游戏...",
	sgdbLookup: "正在 SteamGridDB 中搜索 {{count}} 款游戏...",
	extractingSteamMetadata: "正在提取 Steam 元数据...",
	fetchingEnhancedMetadata: "正在获取增强元数据...",
	lookingUpUnifiDB: "正在获取 unifiDB 元数据...",
	fetchingMetacriticData: "正在获取 Metacritic 数据...",
	fetchingCompatData: "正在检查兼容性...",
	checkingExistingArtwork: "正在检查现有封面图...",
	cleaningOrphanedArtwork: "正在清理孤立的封面图...",
	retryingMissingArtwork: "正在为 {{synced}} / {{total}} 款缺失封面的游戏重试下载...",
	savingShortcuts: "正在保存快捷方式...",
	completed: "同步完成！",
	extractingMetadata: "正在为 {{game}} 提取元数据...",
	cooldown: "请等待 {{secs}} 秒",
	artworkPhase: "封面图 {{done}} / {{total}}"
};
var force_sync$2 = {
	migratingOldInstallations: "强制同步: 正在迁移旧版安装...",
	fetchingGameLists: "强制同步: 正在获取游戏列表...",
	checkingInstalledGames: "强制同步: 正在检查已安装的游戏...",
	rewritingShortcuts: "强制同步: 正在重写快捷方式...",
	cancelledShortcutsSaved: "强制同步已取消 - 快捷方式已保存",
	writingShortcuts: "强制同步: 正在写入快捷方式...",
	clearingProton: "强制同步: 正在清理 Proton 兼容数据...",
	completed: "强制同步已完成！已更新 {{updated}} 款游戏，获取了 {{artwork}} 张封面。"
};
var artwork$2 = {
	checking: "正在检查封面图...",
	queueRefresh: "正在将所有游戏加入队列以刷新封面...",
	downloadProgress: "已下载封面: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$2 = {
	greatOnDeck: "Deck 推荐",
	allGames: "全部游戏",
	installed: "已安装",
	nonSteam: "非 Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$2 = {
	error: "Unifideck 错误",
	errorLoadingView: "无法加载统一的库视图",
	checkConsole: "有关详细信息，请检查浏览器控制台",
	errorLoadingGames: "加载游戏时出错",
	tryReloading: "请尝试重新加载插件或检查控制台",
	storeLabel: "商店:",
	allStores: "所有商店",
	searchPlaceholder: "搜索游戏...",
	library: "库"
};
var gameInfoPanel$2 = {
	loading: "正在加载游戏信息...",
	noMetadata: "此游戏无可用元数据。",
	noTestResults: "无详细测试结果可用。",
	buttons: {
		details: "详细信息",
		synopsis: "剧情大纲",
		storePage: "商店页面",
		dlc: "DLC",
		communityHub: "社区中心",
		pointsShop: "点数商店",
		discussions: "讨论区",
		guides: "指南",
		support: "客服支持",
		install: "安装",
		uninstall: "卸载",
		cancel: "取消",
		achievements: "成就"
	},
	labels: {
		store: "商店:",
		name: "名称:",
		size: "大小:",
		developer: "开发商:",
		publisher: "发行商:",
		released: "发行时间:",
		metacritic: "Metacritic 评分:",
		status: "状态:",
		spaceRequired: "所需空间",
		lastPlayed: "上次运行",
		playTime: "运行时间",
		never: "无记录"
	},
	compatibility: {
		modalTitle: "Steam Deck 兼容性",
		viewOnProtonDb: "在 ProtonDB 上查看",
		close: "关闭",
		verified: "已验证",
		playable: "可玩",
		unsupported: "不支持",
		unknown: "未知"
	},
	failedToLoadMetadata: "加载元数据失败",
	lastSession: "上次游玩：解锁了 {{names}}"
};
var gameDetailsSettings$2 = {
	title: "游戏详情页面视图",
	simple: "精简",
	detailed: "详细"
};
var gogLanguageModal$2 = {
	title: "选择语言",
	description: "选择要为 {{title}} 下载的语言",
	label: "语言",
	install: "安装",
	cancel: "取消"
};
var accountSwitch$2 = {
	title: "检测到账号变更",
	description: "您已登录不同的 Steam 账号。您希望如何处理 Unifideck 的数据？",
	migrate: "迁移 Unifideck 数据",
	migrateDescription: "从上一个账号中复制快捷方式及封面",
	freshStart: "全新开始",
	freshStartDescription: "清空已保存的商店登录状态，以便连接其他 Epic/GOG/Amazon/Ubisoft 账号",
	skip: "跳过",
	toastMigrateTitle: "迁移已完成",
	toastMigrateBody: "已迁移 {{shortcuts}} 个快捷方式，{{artwork}} 张封面",
	toastClearTitle: "已清除认证凭据",
	toastClearBody: "商店的登录状态已重置。您现在可以登录其他账号。"
};
var ubisoftAuth$2 = {
	title: "登录 Ubisoft Connect",
	description: "登录 Ubisoft Connect 账号以绑定您的游戏库。",
	signInViaUpc: "通过 Ubisoft Connect 登录",
	upcDescription: "Ubisoft Connect 将会打开以进行登录。登录成功后，系统会自动导入会话凭据。",
	launchingUpc: "正在启动 Ubisoft Connect...",
	waitingForLogin: "等待登录中...",
	retryLaunch: "重试启动",
	cancel: "取消",
	launchFailed: "启动 Ubisoft Connect 失败。请重试。",
	signingIn: "正在登录...",
	emailLabel: "电子邮箱",
	passwordLabel: "密码",
	signIn: "登录",
	twoFactorTitle: "双重认证",
	twoFactorDescription: "请输入您手机两步验证 App 中显示的动态验证码。",
	twoFactorLabel: "验证码",
	verify: "验证",
	verifying: "正在验证...",
	errorEmptyFields: "请输入邮箱和密码",
	errorEmpty2FA: "请输入双重认证验证码",
	errorLoginFailed: "登录失败。请检查您的凭据是否正确。",
	errorInvalid2FA: "验证码错误。请重试。",
	showPassword: "显示密码",
	hidePassword: "隐藏密码",
	doubleLoginWarning: "您可能需要登录 Ubisoft 两次。"
};
var relativeDate$2 = {
	today: "今天",
	yesterday: "昨天"
};
var common$2 = {
	thisGame: "此游戏",
	game: "游戏",
	unknown: "未知",
	ok: "确定",
	cancel: "取消",
	working: "正在处理…",
	loading: "正在加载…",
	free: "免费",
	close: "关闭"
};
var info$2 = {
	expandDetails: "展开详情",
	collapseDetails: "收起详情"
};
var microsoft$2 = {
	notCompatible: "此游戏格式与 SteamOS 不兼容",
	playOnCloud: "云端游玩",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "登录您的 Microsoft / Xbox 账号",
	accountConnected: "Microsoft 账号已绑定",
	loggedOut: "Microsoft Store 已退出登录",
	xcloudNotInstalled: "xCloud 游戏无需在本地安装",
	chromiumRequired: "需要 Microsoft Edge",
	chromiumRequiredMessage: "登录过程需要 Microsoft Edge 浏览器。是否自动下载并安装？(此操作仅需执行一次)。",
	chromiumInstallButton: "安装 Microsoft Edge",
	chromiumInstalling: "正在安装 Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge 安装成功！",
	chromiumAlreadyInstalled: "已安装兼容的浏览器",
	chromiumInstallFailed: "安装 Microsoft Edge 失败。请尝试手动安装: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "安装 Microsoft Edge 超时。请检查您的网络连接。",
	browserAlreadyInstalled: "已安装兼容的浏览器",
	browserInstalled: "Microsoft Edge 已成功安装！",
	browserInstallFailed: "安装 Microsoft Edge 失败。请尝试手动安装: flatpak install com.microsoft.Edge",
	flatpakNotFound: "系统未检测到 Flatpak 环境",
	signInRequired: "需要登录以进行游玩",
	syncSkippedNoSubscription: "未检测到有效的 Xbox Game Pass 会员资格。xCloud 游戏库同步已被跳过。",
	syncSkippedTierUnknown: "检测到 Xbox Game Pass 订阅，但未能确认会员级别。为了避免显示无法运行的游戏，已跳过同步。",
	subscriptionCheckFailed: "无法验证 Xbox Game Pass 订阅状态。已跳过 xCloud 同步。请在网络连接稳定时重试。",
	subscriptionDetected: "已检测到有效的 Xbox Game Pass {{tier}} 订阅。"
};
var playButton$2 = {
	play: "运行",
	install: "安装",
	cancel: "取消",
	retry: "重试",
	downloadFailed: "下载失败",
	installing: "安装中",
	controllerConfig: "控制器配置",
	appSettings: "应用设置"
};
var play$2 = {
	play: "开始游玩",
	resume: "继续",
	stop: "停止",
	cancel: "取消",
	cancelling: "正在取消…",
	update: "更新",
	uninstall: "卸载",
	queued: "队列中",
	downloading: "下载中",
	extracting: "解压中",
	verifying: "正在验证",
	finalizing: "正在完成安装",
	cancelConfirmTitle: "取消下载？",
	cancelConfirmBody: "您确定要取消安装 {{title}} 吗？未完成下载的文件将被删除。",
	cancelConfirmConfirm: "取消下载",
	cancelConfirmCancel: "继续下载",
	playOnCloud: "云端游玩",
	cloudSave: {
		autoPullOn: "启动时自动下载云端存档。",
		close: "关闭",
		download: "下载云端存档",
		filesCount: "{{count}} 个文件",
		label: "云端存档",
		menuTitle: "云端存档 — {{game}}",
		noCloudData: "未知",
		noCloudSupport: "不支持云端存档",
		setLocation: "设置存档位置",
		setLocationHint: "选择此游戏存储其存档的文件夹。",
		setLocationTitle: "选择此游戏的存档文件夹",
		statusCloudAvailable: "云端存档可用",
		statusNoSaves: "无本地存档",
		statusSyncing: "正在同步云端存档…",
		statusUnresolved: "未找到存档位置 — 此游戏可能无法进行同步。",
		unresolvedHint: "启动游戏一次以便我们检测其存档文件夹，或手动设置位置。",
		upload: "上传本地存档"
	},
	exe: {
		browse: "浏览…",
		changeFailed: "无法更改可执行文件",
		changed: "可执行文件已更改",
		defaultTag: "(默认)",
		menuItem: "更改可执行文件…",
		reset: "重置为默认可执行文件",
		resetButton: "重置为默认",
		subtitle: "选择此游戏启动的文件（例如：跳过启动器并直接运行游戏）。工作目录和云端存档不受影响。",
		title: "更改可执行文件 — {{game}}",
		unavailable: "无法读取此游戏的文件夹。"
	}
};
var playMeta$2 = {
	spaceRequired: "所需空间",
	installedSize: "占用空间",
	lastPlayed: "上次运行",
	neverPlayed: "从未运行过",
	hoursShort: "小时",
	minutesShort: "分钟",
	played: "已玩"
};
var downloads$2 = {
	title: "下载",
	empty: "没有正在进行的下载。从您的库中安装游戏即可在此处查看。",
	current: "正在下载",
	queued: "排队队列",
	finished: "最近完成",
	cancel: "取消",
	removeFromQueue: "移出队列",
	play: "游玩",
	outcome: {
		queued: "队列中",
		downloading: "下载中",
		completed: "已完成",
		cancelled: "已取消",
		error: "错误"
	}
};
var navigation$2 = {
	loading: "正在加载...",
	storesTitle: "商店连接",
	syncTitle: "同步游戏库",
	languageTitle: "语言设置"
};
var security$2 = {
	panelTitle: "安全安全",
	counters: {
		label: "计数器",
		empty: "自启动以来未检测到安全事件"
	},
	log: {
		label: "审计日志",
		entryCount: "{{count}} 个记录",
		entryCount_other: "{{count}} 个记录",
		emptyFiltered: "没有与当前过滤匹配的日志事件"
	},
	filter: {
		label: "过滤",
		all: "全部事件"
	},
	timeline: {
		label: "24小时安全事件走势 ({{count}} 个事件)",
		label_other: "24小时安全事件走势 ({{count}} 个事件)",
		axisStart: "-24小时",
		axisEnd: "当前"
	},
	relativeTime: {
		seconds: "{{count}} 秒前",
		minutes: "{{count}} 分钟前",
		hours: "{{count}} 小时前",
		days: "{{count}} 天前"
	},
	bruteforce: {
		warningTitle: "警告：检测到暴力破解行为 (警告级别)",
		escalationTitle: "警报：检测到暴力破解行为 (严重等级)",
		detail: "在过去的 {{window}} 秒内，解密失败达到了 {{failures}} 次。阈值设定：{{warning}} (警告) / {{escalation}} (警报)。",
		resetButton: "确认并重置检测器"
	},
	actions: {
		clearLog: "清空日志与计数器"
	},
	events: {
		tokenEncrypted: "凭据已加密",
		tokenDecrypted: "凭据已解密",
		decryptFailed: "解密失败",
		tokenFileMigrated: "凭据文件已迁移",
		legacyPlaintextDetected: "检测到旧版明文数据",
		authFlowStarted: "已启动授权流程",
		authFlowCompleted: "已完成授权流程",
		authFlowFailed: "授权流程失败",
		permissionsCheck: "权限检查完成",
		permissionsRepaired: "权限已成功修复",
		bruteforceSuspected: "疑似暴力破解行为",
		deviceResetDetected: "检测到设备重置事件",
		fingerprintInitialized: "指纹设备已初始化",
		externalAuthCheckFailed: "外部连通认证确认失败",
		configValidationCompleted: "配置项有效性核对完成",
		configValidationFailed: "配置项有效性核对失败"
	},
	configValidation: {
		degradedTitle: "配置错误：功能受限模式运行中",
		detail: "启动阶段确认了 {{count}} 个配置异常",
		detail_other: "启动阶段确认了 {{count}} 个配置异常",
		sourceDefaults: "默认选项",
		sourceUser: "用户设定",
		sourceMerged: "已合并项",
		overflow: "+ 另有 {{count}} 个错误",
		overflow_other: "+ 另有 {{count}} 个错误"
	}
};
var cloudSyncBehavior$2 = {
	_comment: "配置面板，供用户针对每一个商店配置云端存档同步错误展示策略。其中的 5 项 store_label 与 CloudSyncBehaviorPanel.tsx 的行渲染相对应。Microsoft 已略去，因为 xCloud 不需要同步本地文件。",
	title: "云端存档同步行为",
	description: "设定当游戏运行前下载云存档、或游戏退出后上传云存档发生问题时，Unifideck 的反馈方案。",
	loadError: "无法载入当前配置",
	saveError: "保存配置失败，请重新尝试",
	store_label: {
		"default": "默认设置 (全部商店)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "静默 (仅记入日志文件)",
		toast: "弹出提示通知"
	}
};
var cloudSync$2 = {
	_comment: "同步出现错误时前端呈现 of 提示。后端抛出 of error_i18n_key 决定映射到哪一个 cloudSync.error.<code> 上。",
	error: {
		network_unreachable: "无法连接到网络",
		auth_expired: "授权已失效，请重登账号",
		forbidden: "请求被拒绝 (无权限)",
		quota_exceeded: "云存档存储容量已超限",
		server_error: "商店云端服务器异常",
		timed_out: "网络通信超时",
		disk_full: "磁盘空间不足",
		disk_space_low: "已跳过：可用磁盘空间偏低 (请释放空间后再试)",
		permission_denied: "权限被拒绝",
		cancelled: "操作已取消",
		unknown: "未预料的系统错误"
	},
	unavailableNativeDep: "此系统上的云端存档已关闭 — 其 Python ({{python}}) 缺少必要的组件。您的本地存档不受影响；请在系统更新后重新构建/重新安装插件以恢复同步。"
};
var launchLogs$2 = {
	_comment: "LaunchLogsModal 使用 of 国际化文案。弹窗通过 launcher 出错通知中附加 of unifideck://show-logs/<launch_id> 协议唤起。它用于读取保存在 ~/.local/share/unifideck/launches/<launch_id>.log 中的单次运行日志，并提供带等级色彩的分类渲染展示。",
	title: "启动日志查询 — {{launchId}}",
	close: "关闭",
	copyAll: "全部复制",
	"export": "导出到文件",
	copied: "已成功复制到剪贴板",
	copyFailed: "复制操作失败",
	exporting: "正在导出…",
	exportedTo: "已成功导出至 {{path}}",
	exportFailed: "文件导出出错: {{error}}",
	loading: "正在加载运行日志…",
	notAvailable: "本次启动的日志数据已不复存在 (已超 7 天的日常存储保留期限)。",
	error: "未能读取日志内容: {{error}}",
	fileInfo: "{{path}} — 当前显示 {{shown}} 行，总计 {{total}} 行",
	filterLabel: "按级别显示",
	filterInfo: "信息",
	filterWarning: "警告",
	filterError: "异常",
	nextError: "定位至下个异常 ({{current}}/{{total}})",
	noErrors: "未检索到错误信息",
	allFiltered: "因当前过滤规则，所有日志行已全部被隐藏。请至少勾选一个日志等级以查看内容。"
};
var settings$2 = {
	_comment: "UnifideckSettingsPanel 基础操控。其中的 refreshAll 用于一次性触发所有启用商店的后台同步任务 (sync_all)。游戏库管理面板会关注 SYNC_PROGRESS 推送来实时展现目前进度。",
	storeConnections: "绑定商店账号",
	librarySync: "同步游戏库",
	language: "语言",
	languageAuto: "自适应系统语言",
	refreshAll: {
		sectionTitle: "游戏库数据操作",
		button: "同步全部已连接的游戏库",
		buttonBusy: "正在启动后台同步…",
		startedTitle: "同步指令发送成功",
		startedBody: "所有商店的数据同步任务目前均在后台静默执行中。",
		startedSingle: "{{store}} 商店的数据同步目前正在后台进行中。",
		failedTitle: "同步指令派发失败"
	}
};
var saveFolder$2 = {
	_comment: "SaveFolderModal of 提示信息。通常在云同步失效且玩家试图手动定位/查看当前本地有哪些可用文件时，经由 unifideck://open-save-folder/<store>/<game_id> 协议调起该弹框。count 和 size 会插值；truncated 在文件数目触发上限而仅显示部分列表时启用。",
	title: "查看本地存档 — {{store}} / {{gameId}}",
	close: "关闭",
	copyPath: "复制文件夹路径",
	copyFileList: "复制文件明细列表",
	depthLabel: "目录深度",
	filterLabel: "关键字查找",
	loading: "正在读取本地目录信息…",
	noSavesYet: "未找到本地保存的存档内容。首次进行云存档成功同步后系统将为您建立相应文件夹。",
	aggregate: "总共包含 {{count}} 个文件，合并占用 {{size}}",
	truncated: "另有 {{count}} 个文件 (占用 {{size}}) 未能在下表中完全呈现，请缩窄筛选条件。",
	error: "本地存档读取操作发生异常: {{error}}"
};
var library$2 = {
	_comment: "库详情中的针对每一个游戏的管理功能，包含自动断路判定提示，以及多次故障后提供的绕过测试与数据初始化清空选项。",
	circuitBreaker: {
		_comment: "错误熔断 of 交互文案。仅当在特定的熔断窗口中出错次数触发设定的 circuit_breaker.failures_threshold (默认值是 600 秒 3 次) 时提示该状态。强行启动允许暂时绕过；重置会将统计清零。",
		badgeRecent: "最近发生了 {{count}} 次运行失败",
		badgeRecent_other: "最近发生了 {{count}} 次运行失败",
		badgeTooltip: "由于近来运行失败已达 {{count}} 次，自动熔断机制已介入并暂停了本游戏的自动化启动流程。",
		forceLaunchButton: "强制运行 (忽略拦截)",
		resetButton: "清零错误次数",
		failureKind: {
			fastBoot: "启动后秒退崩溃",
			launcherError: "商店客户端配置异常"
		}
	}
};
var pickStorage$2 = {
	title: "安装路径选择",
	customOption: "定位至其他路径...",
	customLabel: "自定义路径",
	change: "更改设置...",
	gameSize: "安装预占空间"
};
var restart$2 = {
	title: "同步完成！",
	body: "请重启 Steam 以应用您的新游戏和封面",
	confirm: "立即重启",
	later: "稍后"
};
var updater$2 = {
	checkButton: "检查更新",
	checkCompleteMessage: "已成功获取最新版本信息。",
	checkCompleteTitle: "更新检查完成",
	checkFailedTitle: "检查失败",
	checkingButton: "检查中...",
	currentTitle: "当前版本",
	downgradeButton: "降级至 v{{version}}",
	installFailedTitle: "安装失败",
	installTimeoutMessage: "Decky Loader 无响应 — 安装可能已停滞或失败。请重试。",
	installedLabel: "已安装",
	installingButton: "正在安装...",
	installingTitle: "安装插件中",
	latestLabel: "最新版本",
	modalTitle: "UNIFIDECK v{{version}} — 发行说明",
	noBackend: "Decky 后端服务不可用。",
	noReleaseNotes: "无可用发行说明。",
	reinstallButton: "重新安装 v{{version}}",
	refreshFailedMessage: "无法验证最新版本信息。请检查网络连接后重试。",
	releaseGoneMessage: "此版本已不再可用。请检查更新后重新选择。",
	releaseNotesButton: "发行说明",
	stageDownload: "下载中…",
	stageFinish: "正在完成…",
	stageInstall: "安装中…",
	stageOpen: "读取包中…",
	stageParse: "正在验证…",
	stageRemove: "正在移除旧版本…",
	stageStart: "正在启动…",
	stageWorking: "安装中…",
	titleLoading: "正在检查版本",
	typeDowngrade: "降级至",
	typeReinstall: "重新安装",
	typeUpdate: "升级至",
	updateButton: "升级至 v{{version}}"
};
var achievements$2 = {
	unlockedTitle: "成就已解锁",
	unlockedBody: "{{name}}"
};
var achievementsModal$2 = {
	title: "成就",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "刷新",
	close: "关闭",
	loading: "正在加载成就…",
	empty: "此游戏没有成就。",
	hidden: "隐藏成就",
	unlockedOn: "{{date}} 解锁",
	error: "无法加载成就。",
	errorOffline: "连接到互联网以查看成就。",
	errorAuth: "登录此游戏的商店以查看成就。",
	errorUnavailable: "此游戏不支持成就。"
};
var collectionSettings$2 = {
	title: "STEAM 收藏夹",
	enabled: "收藏夹已启用",
	disabled: "收藏夹已禁用"
};
var zhCN = {
	_comment: _comment$2,
	tabs: tabs$2,
	auth: auth$2,
	storage: storage$2,
	storageSettings: storageSettings$2,
	gameGrid: gameGrid$2,
	downloadsTab: downloadsTab$2,
	cloudSave: cloudSave$2,
	storeConnections: storeConnections$2,
	librarySync: librarySync$2,
	cleanup: cleanup$2,
	captureLogs: captureLogs$2,
	installButton: installButton$2,
	confirmModals: confirmModals$2,
	toasts: toasts$2,
	uninstallModal: uninstallModal$2,
	languageSettings: languageSettings$2,
	errors: errors$2,
	sync: sync$2,
	force_sync: force_sync$2,
	artwork: artwork$2,
	deckTabs: deckTabs$2,
	unifiedLibrary: unifiedLibrary$2,
	gameInfoPanel: gameInfoPanel$2,
	gameDetailsSettings: gameDetailsSettings$2,
	gogLanguageModal: gogLanguageModal$2,
	accountSwitch: accountSwitch$2,
	ubisoftAuth: ubisoftAuth$2,
	relativeDate: relativeDate$2,
	common: common$2,
	info: info$2,
	microsoft: microsoft$2,
	playButton: playButton$2,
	play: play$2,
	playMeta: playMeta$2,
	downloads: downloads$2,
	navigation: navigation$2,
	security: security$2,
	cloudSyncBehavior: cloudSyncBehavior$2,
	cloudSync: cloudSync$2,
	launchLogs: launchLogs$2,
	settings: settings$2,
	saveFolder: saveFolder$2,
	library: library$2,
	pickStorage: pickStorage$2,
	restart: restart$2,
	updater: updater$2,
	achievements: achievements$2,
	achievementsModal: achievementsModal$2,
	collectionSettings: collectionSettings$2
};

var _comment$1 = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs$1 = {
	settings: "設定",
	downloads: "下載"
};
var auth$1 = {
	connect: "連結",
	disconnect: "中斷連結",
	status: {
		connected: "已連結",
		disconnected: "未連結",
		checking: "正在檢查…",
		error: "錯誤 — 請檢查記錄檔"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage$1 = {
	title: "儲存空間",
	defaultUpdated: "預設儲存位置已更新",
	isDefault: "預設",
	setDefault: "設為預設",
	customPath: "自訂路徑",
	up: "↑ 返回上層",
	refresh: "重新整理",
	useThisPath: "使用此路徑"
};
var storageSettings$1 = {
	title: "下載設定",
	installLocationLabel: "安裝位置",
	installLocationDescription: "新遊戲將被下載到的位置",
	pathLabel: "路徑",
	loading: "正在載入儲存選項...",
	toastUpdatedTitle: "儲存位置已更新",
	toastUpdatedBody: "新遊戲將安裝到 {{location}}",
	toastFailedTitle: "更新失敗",
	toastFailedBody: "{{error}}",
	unknown: "未知",
	browseButton: "瀏覽...",
	clearCustom: "清除自訂位置",
	customPathSet: "自訂安裝位置已設定",
	newFolder: "新增資料夾",
	newFolderCreate: "建立",
	selectFolder: "使用此資料夾",
	emptyDirectory: "空資料夾",
	showHidden: "顯示隱藏檔案",
	selectDevice: "選擇儲存裝置",
	sortAZ: "A-Z",
	sortZA: "Z-A",
	sortModifiedNewest: "修改時間 (最新)",
	sortModifiedOldest: "修改時間 (最舊)",
	sortCreatedNewest: "建立時間 (最新)",
	sortCreatedOldest: "建立時間 (最舊)",
	sortSizeLargest: "大小 (最大)",
	sortSizeSmallest: "大小 (最小)",
	"validationErrors.notAbsolute": "路徑必須是絕對路徑",
	"validationErrors.systemDir": "無法使用系統目錄",
	"validationErrors.notWritable": "該位置無法寫入",
	"validationErrors.notFound": "目錄不存在",
	"validationErrors.homeRoot": "無法使用使用者根目錄",
	freeSpaceLabel: "{{label}} (剩餘 {{freeSpace}} GB)"
};
var gameGrid$1 = {
	loading: "正在載入遊戲...",
	noGamesFound: "未找到遊戲",
	noGamesHint: "請嘗試在 Unifideck 設定中同步您的遊戲庫",
	showingGames: "正在顯示 {{count}} 款遊戲",
	installed: "已安裝",
	notInstalled: "未安裝",
	playtime: "已玩 {{hours}} 小時 {{minutes}} 分鐘"
};
var downloadsTab$1 = {
	currentDownload: "目前下載",
	queuedDownloads: "佇列中 ({{count}})",
	recentlyCompleted: "最近完成",
	noActiveDownloads: "沒有進行中的下載",
	noDownloads: "沒有下載任務。從您的庫中安裝遊戲即可在此處查看。",
	loadingLabel: "正在載入...",
	loadingMessage: "正在獲取下載佇列...",
	confirmCancelTitle: "確認取消",
	confirmCancelDescription: "您確定要取消下載 {{game}} 嗎？",
	confirmCancelYes: "是",
	confirmCancelNo: "否",
	cancelDownload: "取消",
	toastDownloadCancelledTitle: "下載已取消",
	toastDownloadCancelledBody: "下載任務已從佇列中移除。",
	toastCancelFailedTitle: "取消失敗",
	toastCancelFailedBody: "無法取消下載: {{error}}",
	status: {
		queued: "佇列中",
		downloading: "下載中",
		completed: "完成",
		cancelled: "已取消",
		error: "錯誤"
	},
	preparingDownload: "正在準備下載...",
	phaseExtracting: "正在解壓縮 {{game}}...",
	phaseVerifying: "正在驗證 {{game}}...",
	internalStorage: "內部儲存 (剩餘 {{freeSpace}} GB)",
	sdCard: "SD 卡 (剩餘 {{freeSpace}} GB)",
	customLocation: "自訂: {{path}} (剩餘 {{freeSpace}} GB)",
	extractingLabel: "正在解壓縮...",
	verifyingLabel: "正在驗證...",
	updateQueuedLabel: "更新排隊中",
	downloadQueuedLabel: "下載排隊中",
	downloadingUpdateLabel: "正在下載更新",
	downloadingLabel: "正在下載...",
	finalizingInstallation: "正在完成安裝...",
	speedMbps: "{{speed}} MB/s",
	etaLabel: "剩餘時間: {{eta}}",
	preparingMessage: "正在初始化容器並同步雲端存檔...",
	preparingLabel: "正在設定遊戲...",
	installingViaUpcLabel: "正在透過 Ubisoft Connect 安裝",
	extractingMessage: "正在解壓縮檔案...",
	verifyingMessage: "正在驗證檔案... {{pct}}%",
	installingViaUpcMessage: "請依照 Ubisoft Connect 視窗的提示完成安裝..."
};
var cloudSave$1 = {
	title: "雲端存檔衝突",
	description: "您的本機資料與 {{game}} 的雲端存檔不同",
	local: "本機:",
	cloud: "雲端:",
	newer: "(較新)",
	useCloud: "使用雲端存檔",
	useLocal: "使用本機存檔",
	unknownDate: "未知",
	toastUsingCloudTitle: "使用雲端存檔中",
	toastUsingCloudBody: "正在下載 {{game}} 的雲端存檔",
	toastUsingLocalTitle: "使用本機存檔中",
	toastUsingLocalBody: "正在上傳 {{game}} 的本機存檔",
	toastErrorTitle: "錯誤",
	toastErrorBody: "無法解決存檔衝突",
	filesSize: "{{count}} 個檔案 · {{size}}",
	uploadBlockedEmpty: "已略過 {{game}} 的雲端上傳 — 找不到本機存檔",
	uploadSkippedTitle: "已略過雲端上傳",
	uploadSkippedBody: "{{game}} 暫無可上傳的本機存檔。"
};
var storeConnections$1 = {
	title: "商店連接",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "已連結",
	checking: "正在檢查...",
	authenticate: "登入",
	legendaryNotInstalled: "⚠️ 正在安裝 Epic Games 用戶端...",
	nileNotInstalled: "⚠️ 未找到 Amazon Games 用戶端",
	error: "錯誤 — 請檢查記錄檔"
};
var librarySync$1 = {
	title: "遊戲庫同步",
	syncLibraries: "同步遊戲庫",
	forceSync: "強制同步",
	syncing: "正在同步...",
	cancelSync: "取消同步",
	retryStatusCheck: "重試狀態檢查",
	gamesSynced: "已同步遊戲 {{synced}} / {{total}}",
	artworkDownloaded: "已下載封面 {{synced}} / {{total}}",
	metadataExtracted: "已提取元資料 {{synced}} / {{total}}",
	steamMetadataDownloaded: "Steam 元資料: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "unifiDB 元資料: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "Metacritic 資料: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "RAWG 元資料: {{synced}} / {{total}}",
	compatChecked: "相容性資料: {{synced}} / {{total}}",
	cancelling: "正在取消…",
	error: "錯誤"
};
var cleanup$1 = {
	title: "清理",
	deleteAll: "刪除所有 UNIFIDECK 庫和快取",
	modalTitle: "刪除所有 Unifideck 資料？",
	modalDescription: "這將刪除所有的 Unifideck 捷徑、Steam 封面圖、儲存的商店登入狀態和快取資料。已安裝的遊戲仍將保留在磁碟上，可以重新同步。啟用下方的破壞性模式可同時刪除已安裝的遊戲檔案。",
	deleteFilesLabel: "同時刪除已安裝的遊戲檔案 (破壞性)",
	deleteFilesDescription: "刪除安全根目錄下每個 Unifideck 遊戲在磁碟上的安裝目錄。",
	confirmDelete: "是的，刪除全部",
	cancel: "取消",
	deleting: "正在刪除..."
};
var captureLogs$1 = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (zh-TW).",
	title: "收集記錄",
	description: "將所有 Unifideck 記錄收集到下載資料夾中的單一 zip 檔案，方便附加到錯誤報告。",
	capture: "收集記錄",
	capturing: "正在收集記錄…",
	savedTo: "已儲存到你的下載資料夾",
	bundleInfo: "{{files}} 個檔案 · {{size}}",
	partialNote: "有 {{count}} 個檔案無法讀取。此壓縮檔仍可使用。",
	partialNote_other: "有 {{count}} 個檔案無法讀取。此壓縮檔仍可使用。",
	copyPath: "複製路徑",
	modalTitle: "建立診斷壓縮檔？",
	modalDescription: "Unifideck 會將記錄收集到下載資料夾中的單一 zip 檔案，方便你附加到錯誤報告。",
	modalIncludes: "包含：外掛與遊戲啟動記錄、同步與安裝歷程、你的遊戲名稱與安裝路徑，以及裝置資訊（型號、作業系統與儲存空間）。",
	modalExcludes: "絕不包含：你的密碼、商店登入權杖或瀏覽器 Cookie。",
	modalConfirm: "建立壓縮檔",
	cancel: "取消"
};
var installButton$1 = {
	play: "執行",
	resume: "繼續",
	install: "安裝 {{title}}",
	installNative: "安裝",
	uninstall: "解除安裝 {{title}}",
	cancel: "✖ 取消",
	processing: "正在處理...",
	checking: "正在檢查...",
	size: "{{size}} GB",
	update: "更新",
	updateAvailable: "有可用更新",
	updateAvailableDescription: "新版本已準備好安裝。"
};
var confirmModals$1 = {
	installTitle: "確認安裝",
	installDescription: "您確定要安裝 {{title}} 嗎？",
	uninstallTitle: "確認解除安裝",
	uninstallDescription: "您確定要解除安裝 {{title}} 嗎？",
	cancelTitle: "確認取消",
	cancelDescription: "您確定要取消下載 {{title}} 嗎？",
	forceSyncTitle: "強制同步設定",
	forceSyncDescription: "您想重新同步所有封面嗎？這會覆寫您對封面的任何手動變更。選擇「保留封面」將僅下載缺失的封面。",
	resyncArtwork: "重新同步封面",
	keepArtwork: "保留封面",
	steamRestartTitle: "同步完成！",
	steamRestartDescription: "請重啟 Steam 以套用您的新遊戲和封面",
	steamRestartDescriptionStore: "請重啟 Steam 以套用您的新遊戲和 {{store}} 封面",
	steamRestartCleanupTitle: "清理完成！",
	steamRestartCleanupDescription: "請重啟 Steam 以完全移除已刪除的遊戲和封面",
	restartNow: "立即重啟",
	later: "稍後",
	ubisoftInstallTitle: "需要 Ubisoft Connect",
	ubisoftInstallDescription: "這會載入安裝 {{title}} 所需的 Ubisoft Connect 用戶端。",
	ubisoftInstallConfirm: "打開 Ubisoft Connect",
	yes: "是",
	no: "否",
	amazonUpdateTitle: "需要重新完整下載",
	amazonUpdateDescription: "Amazon 更新需要完全重新下載 {{title}}。這可能需要一些時間。是否繼續？",
	updateConfirm: "更新"
};
var toasts$1 = {
	downloadStarted: "下載已開始。請檢查 UNIFIDECK > 下載",
	downloadQueued: "下載已排隊",
	downloadQueuedBody: "{{title}} 已加入佇列。",
	downloadFailedUnknown: "未知錯誤。",
	updateQueued: "更新已排隊",
	uninstallDone: "解除安裝已完成。",
	ubisoftLauncherOpening: "正在打開 Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "正在為 {{title}} 打開 Ubisoft Connect。請在此處安裝遊戲，然後返回啟動遊戲。",
	ubisoftLauncherAlreadyOpenMessage: "此安裝已打開 Ubisoft Connect。",
	ubisoftLauncherOpenFailed: "無法打開 Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "無法打開 Ubisoft Connect 以進行安裝。",
	multipartDetected: "偵測到多部分下載",
	multipartMessage: "請耐心等待完成",
	downloadFailed: "下載失敗",
	downloadFailedMessage: "無法啟動下載。",
	downloadCancelled: "下載已取消",
	downloadCancelledMessage: "{{title}} 的下載已取消。",
	cancelFailed: "取消失敗",
	cancelFailedMessage: "無法取消下載。",
	uninstalling: "正在解除安裝...",
	uninstallingMessage: "正在解除安裝 {{title}}...",
	uninstallingMessageProton: "正在解除安裝 {{title}} 及 Proton 檔案...",
	uninstallComplete: "解除安裝已完成",
	uninstallCompleteMessage: "{{title}} 已刪除。",
	uninstallCompleteMessageProton: "{{title}} 已刪除 (包括 Proton 檔案)。",
	installComplete: "安裝已完成！",
	installCompleteMessage: "{{title}} 準備就緒。",
	syncComplete: "同步完成！",
	syncCompleteMessage: "已新增 {{count}} 款遊戲。請重啟 STEAM (完全退出它，而不僅是返回遊戲模式) 以在您的庫中查看它們。",
	forceSyncComplete: "強制同步已完成！",
	forceSyncCompleteMessage: "已更新 {{count}} 款遊戲。請重啟 STEAM 以套用變更。",
	syncCancelled: "同步已取消",
	authTimeout: "認證逾時",
	authTimeoutMessage: "{{store}} 認證已過期。請重試。",
	authFailed: "認證失敗",
	authFailedMessage: "無法啟動認證流程",
	authError: "認證錯誤",
	authConnected: "{{store}} 已連結！",
	authConnectedMessage: "成功通過 {{store}} 認證",
	cleanupStarted: "清理已啟動",
	cleanupStartedMessage: "正在刪除捷徑、封面和儲存的登入狀態。这可能需要一點時間…",
	cleanupSuccessful: "清理成功",
	cleanupSuccessfulMessage: "已刪除 {{games}} 個捷徑，{{artwork}} 個封面檔案，已登出 {{stores}} 個商店，已刪除 {{files}} 個安裝目錄。快取已清空。",
	cleanupNoop: "無可清理內容",
	cleanupNoopMessage: "Unifideck 沒有需要刪除的捷徑、封面或儲存的登入狀態。",
	deleteFailed: "刪除失敗",
	repairPrefixSuccess: "執行環境修復成功",
	repairPrefixSuccessMessage: "Ubisoft Connect 執行環境已重建",
	repairPrefixFailed: "修復失敗",
	repairPrefixFailedMessage: "無法修復執行環境: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "網路錯誤",
		launchRetry: "正在重試啟動",
		retryingUmu: "{{seconds}} 秒後重試 UMU (嘗試次數 {{attempt}}/{{max}})...",
		protonUpgrade: "Proton 升級",
		resettingPrefix: "正在為 {{version}} 重設環境...",
		firstTimeSetup: "首次設定",
		initializingPrefix: "正在初始化 Wine 環境...",
		setupCompleteTitle: "設定完成",
		prefixInitialized: "Wine 環境已初始化",
		setupFallback: "設定",
		fallbackInitialization: "備用初始化處理中...",
		launchingGame: "正在啟動遊戲",
		startingEpicGame: "正在啟動 Epic 遊戲...",
		startingAmazonGame: "正在啟動 Amazon 遊戲...",
		startingGogGame: "正在啟動 GOG 遊戲...",
		startingUbisoftGame: "正在啟動 Ubisoft 遊戲...",
		dependenciesTitle: "正在安裝相依性",
		installingRedistributables: "正在安裝遊戲必需的執行庫...",
		dependenciesReady: "相依性就緒",
		redistributablesInstalled: "執行庫已安裝",
		dependenciesStatus: "相依性狀態",
		checkLogs: "設定完成 (請查看記錄檔以了解詳細資訊)",
		installingProton: "正在安裝 Proton",
		attemptingInstall: "正在嘗試安裝 {{version}}...",
		epicAuthRequired: "需要 Epic 登入",
		epicAuthRequiredMessage: "請在 Unifideck 設定中登入 Epic Games",
		installingRedist: "正在安裝執行庫",
		installingRedistMessage: "正在執行 GOG 執行庫的首次安裝...",
		setupFailed: "設定失敗",
		setupFailedMessage: "安裝失敗，請查看記錄檔了解詳細資訊",
		prerequisitesTitle: "正在安裝預備組件",
		installingPrerequisites: "正在安裝必需的遊戲組件...",
		prerequisitesReady: "預備組件就緒",
		prerequisitesInstalled: "遊戲預備組件已安裝",
		prerequisitesFailed: "預備組件警告",
		prerequisitesFailedMessage: "預备組件可能未能正確安裝",
		browserRequired: "需要 Microsoft Edge",
		signingIn: "正在登入...",
		cloudSyncDownFailed: "無法為 {{store}} 下載雲端存檔 ({{error}})。正在以本機存檔啟動遊戲。",
		cloudSyncUpFailed: "無法為 {{store}} 上傳雲端存檔 ({{error}})。您的進度僅儲存在本機。",
		errorCircuitBreakerOpen: "{{game_key}} 最近啟動失敗了 {{count}} 次。已暫停自動執行。",
		launcherError: "無法啟動 {{game_key}} ({{error_code}})。請查看記錄檔了解詳細資訊。",
		downloadingProton: "正在下載 Proton — 這可能需要幾分鐘...",
		downloadingRuntime: "正在下載 Steam Runtime — 首次啟動可能需要幾分鐘...",
		installingUbisoft: "正在透過 Ubisoft Connect 安裝",
		installingUbisoftMessage: "正在開啟 Ubisoft Connect — 請在啟動器視窗中開始安裝。",
		protonReadyBody: "{{version}} 已可使用",
		protonReadyTitle: "Proton 已就緒",
		protonSwitchedTo: "現在使用 {{version}}",
		signingInUbisoft: "正在登入 Ubisoft Connect",
		signingInUbisoftMessage: "正在開啟 Ubisoft Connect — 請在那裡登入後返回。",
		ubisoftPrefixNotReady: "Ubisoft Connect 尚未就緒",
		ubisoftPrefixNotReadyMessage: "無法為此遊戲準備 Ubisoft Connect 前綴。請重試安裝；若仍然失敗，請重新執行庫同步。",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect 尚未就緒",
		ubisoftAuthPrefixNotReadyMessage: "無法為登入準備 Ubisoft Connect。請稍後重試。",
		ubisoftLaunchIdMissing: "正在開啟 Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "無法解析此遊戲的啟動 ID — 正在開啟 Ubisoft Connect，以便您從庫中啟動它。請重新執行庫同步以修復直接啟動。"
	},
	updateComplete: "更新已完成！",
	updateCompleteMessage: "{{title}} 已更新。",
	updateCancelled: "更新已取消",
	updateCancelledMessage: "{{title}} 的更新已取消。",
	updateFailed: "更新失敗",
	updateFailedMessage: "無法啟動更新流程。",
	storeError: "無法與商店 {{store}} 通訊 ({{errType}})",
	captureLogsStarted: "正在收集記錄…",
	captureLogsStartedMessage: "正在下載資料夾中建立診斷壓縮檔，需要幾秒鐘。",
	captureLogsDone: "已收集記錄",
	captureLogsDoneMessage: "已儲存到 {{path}}（{{size}}，{{files}} 個檔案）。",
	captureLogsPartial: "已收集記錄（部分）",
	captureLogsPartialMessage: "已儲存到 {{path}}（{{size}}，{{files}} 個檔案）。有 {{failures}} 個檔案無法讀取。",
	captureLogsFailed: "無法收集記錄",
	captureLogsBusy: "記錄收集已在進行中",
	captureLogsCopied: "已複製路徑",
	captureLogsCopyFailed: "無法複製路徑",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "釋放空間",
		signInToStore: "登入商店",
		retrySync: "重試",
		retryLibrarySync: "重新整理遊戲庫",
		openSaveFolder: "查看存檔資料夾",
		showLogs: "查看記錄檔"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "無法重新整理 {{store}} 遊戲庫 ({{error}})。您現有的遊戲庫列表保持可見。"
	},
	cloudPullStarted: "正在下載雲端存檔…",
	cloudPushStarted: "正在上傳本機存檔…",
	cloudSavePathFailed: "無法設定存檔位置",
	cloudSavePathSet: "存檔位置已更新",
	cloudPullDone: "已下載雲端存檔",
	cloudPullFailed: "下載雲端存檔失敗",
	cloudPushDone: "已上傳本機存檔",
	cloudPushFailed: "上傳本機存檔失敗"
};
var uninstallModal$1 = {
	title: "確認解除安裝",
	description: "您確定要解除安裝 {{title}} 嗎？",
	deleteProtonLabel: "同時刪除 Proton 檔案",
	deleteProtonDescription: "刪除相容性資料和使用者檔案",
	warningTitle: "警告:",
	warningBody: "這將永久刪除儲存在 Wine 環境下的存檔資料。請確保您有備份。",
	cancel: "取消",
	uninstall: "解除安裝"
};
var languageSettings$1 = {
	title: "語言",
	label: "顯示語言",
	description: "介面以及遊戲下載所用語言 (如支援)",
	autoDetect: "自動 (系統)"
};
var errors$1 = {
	legendaryNotFound: "未找到 Epic Games 用戶端 (Legendary)",
	gogdlNotFound: "未找到 GOG 用戶端 (gogdl)",
	nileNotFound: "未找到 Amazon Games 用戶端 (nile)",
	authUrlFailed: "無法獲取認證 URL",
	authFailed: "認證失敗",
	notAuthenticated: "未認證。請先登入。",
	syncInProgress: "同步已在進行中",
	syncCancelled: "使用者取消了同步",
	gameNotFound: "未找到遊戲",
	gameNotInstalled: "遊戲未安裝",
	installFailed: "安裝失敗 — 請檢查記錄檔",
	installNoDirectory: "安裝已完成，但無法定位遊戲目錄",
	uninstallFailed: "解除安裝失敗",
	lockConflict: "鎖定衝突 — 請重試",
	unknownStore: "未知商店",
	shortcutWriteFailed: "寫入捷徑失敗",
	backgroundSyncDisabled: "背景同步已停用",
	steamGridDbUnavailable: "SteamGridDB 庫不可用",
	deleteSyncInProgress: "正在同步時無法進行刪除操作",
	alreadyInQueue: "已在佇列中",
	download: {
		diskSpace: "可用磁碟空間不足",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "網路錯誤 — 請檢查您的連線",
		authExpired: "登入已過期 — 請重新登入",
		toolMissing: "未找到所需的下載工具",
		processFailed: "下載失敗 — 請重試",
		directoryNotFound: "無法找到已安裝的檔案",
		lockConflict: "另一項操作正在進行中 — 請重試",
		ubisoftClient: "Ubisoft Connect 用戶端錯誤 — 請嘗試修復環境",
		ubisoftClientNotRunning: "Ubisoft Connect 用戶端無回應",
		ubisoftBootstrap: "無法配置 Ubisoft Connect 執行環境",
		ubisoftUmuMissing: "未找到 umu-run — 執行 Ubisoft 遊戲所必需",
		generic: "安裝失敗 — 請檢查記錄檔"
	},
	unknown: "未知錯誤",
	steamAppsUnavailable: "SteamClient.Apps API 不可用"
};
var sync$1 = {
	start: "同步遊戲庫",
	force: "強制同步",
	syncing: "正在同步…",
	cancel: "取消同步",
	cancelling: "正在取消…",
	preparing: "正在準備…",
	downloadingArtwork: "正在為 {{game}} 下載封面圖...",
	fetchingGameLists: "正在獲取遊戲列表...",
	fetchingStore: "正在獲取 {{store}} 遊戲 ({{current}}/{{total}})...",
	cancelledByUser: "使用者取消了同步",
	checkingInstalledGames: "正在檢查已安裝的遊戲...",
	sgdbLookup: "正在 SteamGridDB 中搜尋 {{count}} 款遊戲...",
	extractingSteamMetadata: "正在提取 Steam 元資料...",
	fetchingEnhancedMetadata: "正在獲取增強元資料...",
	lookingUpUnifiDB: "正在獲取 unifiDB 元資料...",
	fetchingMetacriticData: "正在獲取 Metacritic 資料...",
	fetchingCompatData: "正在檢查相容性...",
	checkingExistingArtwork: "正在檢查現有封面圖...",
	cleaningOrphanedArtwork: "正在清理孤立的封面圖...",
	retryingMissingArtwork: "正在為 {{synced}} / {{total}} 款缺失封面的遊戲重試下載...",
	savingShortcuts: "正在保存捷徑...",
	completed: "同步完成！",
	extractingMetadata: "正在為 {{game}} 提取元資料...",
	cooldown: "請等待 {{secs}} 秒",
	artworkPhase: "封面圖 {{done}} / {{total}}"
};
var force_sync$1 = {
	migratingOldInstallations: "強制同步: 正在遷移舊版安裝...",
	fetchingGameLists: "強制同步: 正在獲取遊戲列表...",
	checkingInstalledGames: "強制同步: 正在檢查已安裝的遊戲...",
	rewritingShortcuts: "強制同步: 正在重寫捷徑...",
	cancelledShortcutsSaved: "強制同步已取消 - 捷徑已保存",
	writingShortcuts: "強制同步: 正在寫入捷徑...",
	clearingProton: "強制同步: 正在清理 Proton 相容資料...",
	completed: "強制同步已完成！已更新 {{updated}} 款遊戲，獲取了 {{artwork}} 張封面。"
};
var artwork$1 = {
	checking: "正在檢查封面圖...",
	queueRefresh: "正在將所有遊戲加入佇列以重新整理封面...",
	downloadProgress: "已下載封面: {{synced}}/{{total}} ({{game}})"
};
var deckTabs$1 = {
	greatOnDeck: "Deck 推薦",
	allGames: "全部遊戲",
	installed: "已安裝",
	nonSteam: "非 Steam",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary$1 = {
	error: "Unifideck 錯誤",
	errorLoadingView: "無法載入統一的庫視圖",
	checkConsole: "有關詳細資訊，請檢查瀏覽器主控台",
	errorLoadingGames: "載入遊戲時出错",
	tryReloading: "請嘗試重新載入外掛程式或檢查主控台",
	storeLabel: "商店:",
	allStores: "所有商店",
	searchPlaceholder: "搜尋遊戲...",
	library: "庫"
};
var gameInfoPanel$1 = {
	loading: "正在載入遊戲資訊...",
	noMetadata: "此遊戲無可用元資料。",
	noTestResults: "無詳細測試結果可用。",
	buttons: {
		details: "詳細資訊",
		synopsis: "劇情大綱",
		storePage: "商店頁面",
		dlc: "DLC",
		communityHub: "社群中心",
		pointsShop: "點數商店",
		discussions: "討論區",
		guides: "指南",
		support: "客服支援",
		install: "安裝",
		uninstall: "解除安裝",
		cancel: "取消",
		achievements: "成就"
	},
	labels: {
		store: "商店:",
		name: "名稱:",
		size: "大小:",
		developer: "開發商:",
		publisher: "發行商:",
		released: "發行時間:",
		metacritic: "Metacritic 評分:",
		status: "狀態:",
		spaceRequired: "所需空間",
		lastPlayed: "上次執行",
		playTime: "執行時間",
		never: "無記錄"
	},
	compatibility: {
		modalTitle: "Steam Deck 相容性",
		viewOnProtonDb: "在 ProtonDB 上查看",
		close: "關閉",
		verified: "已驗證",
		playable: "可玩",
		unsupported: "不支持",
		unknown: "未知"
	},
	failedToLoadMetadata: "載入元資料失敗",
	lastSession: "上次遊玩：解鎖了 {{names}}"
};
var gameDetailsSettings$1 = {
	title: "遊戲詳情頁面視圖",
	simple: "精簡",
	detailed: "詳細"
};
var gogLanguageModal$1 = {
	title: "選擇語言",
	description: "選擇要為 {{title}} 下載的語言",
	label: "語言",
	install: "安裝",
	cancel: "取消"
};
var accountSwitch$1 = {
	title: "偵測到帳號變更",
	description: "您已登入不同的 Steam 帳號。您希望如何處理 Unifideck 的資料？",
	migrate: "遷移 Unifideck 資料",
	migrateDescription: "從上一個帳號中複製捷徑及封面",
	freshStart: "全新開始",
	freshStartDescription: "清空已儲存 of 商店登入狀態，以便連結其他 Epic/GOG/Amazon/Ubisoft 帳號",
	skip: "跳過",
	toastMigrateTitle: "遷移已完成",
	toastMigrateBody: "已遷移 {{shortcuts}} 個捷徑，{{artwork}} 張封面",
	toastClearTitle: "已清除認證認證憑據",
	toastClearBody: "商店 of 登入狀態已重設。您現在可以登入其他帳號。"
};
var ubisoftAuth$1 = {
	title: "登入 Ubisoft Connect",
	description: "登入 Ubisoft Connect 帳號以綁定您的遊戲庫。",
	signInViaUpc: "通過 Ubisoft Connect 登入",
	upcDescription: "Ubisoft Connect 將會打開以進行登入。登入成功後，系統會自動匯入工作階段憑據。",
	launchingUpc: "正在啟動 Ubisoft Connect...",
	waitingForLogin: "等待登入中...",
	retryLaunch: "重試啟動",
	cancel: "取消",
	launchFailed: "啟動 Ubisoft Connect 失敗。請重試。",
	signingIn: "正在登入...",
	emailLabel: "電子信箱",
	passwordLabel: "密碼",
	signIn: "登入",
	twoFactorTitle: "雙重認證",
	twoFactorDescription: "請輸入您手機兩步驟驗證 App 中顯示的動態驗證碼。",
	twoFactorLabel: "驗證碼",
	verify: "驗證",
	verifying: "正在驗證...",
	errorEmptyFields: "請輸入信箱和密碼",
	errorEmpty2FA: "請輸入雙重認證驗碼",
	errorLoginFailed: "登入失敗。請檢查您的憑據是否正確。",
	errorInvalid2FA: "驗證碼錯誤。請重試。",
	showPassword: "顯示密碼",
	hidePassword: "隱藏密碼",
	doubleLoginWarning: "您可能需要登入 Ubisoft 兩次。"
};
var relativeDate$1 = {
	today: "今天",
	yesterday: "昨天"
};
var common$1 = {
	thisGame: "此遊戲",
	game: "遊戲",
	unknown: "未知",
	ok: "確定",
	cancel: "取消",
	working: "正在處理…",
	loading: "正在載入…",
	free: "免費",
	close: "關閉"
};
var info$1 = {
	expandDetails: "展開詳情",
	collapseDetails: "收起詳情"
};
var microsoft$1 = {
	notCompatible: "此遊戲格式與 SteamOS 不相容",
	playOnCloud: "雲端遊玩",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "登入您的 Microsoft / Xbox 帳號",
	accountConnected: "Microsoft 帳號已綁定",
	loggedOut: "Microsoft Store 已登出",
	xcloudNotInstalled: "xCloud 遊戲無需在本機安裝",
	chromiumRequired: "需要 Microsoft Edge",
	chromiumRequiredMessage: "登入過程需要 Microsoft Edge 瀏覽器。是否自動下載並安裝？(此動作僅需執行一次)。",
	chromiumInstallButton: "安裝 Microsoft Edge",
	chromiumInstalling: "正在安裝 Microsoft Edge...",
	chromiumInstalled: "Microsoft Edge 安裝成功！",
	chromiumAlreadyInstalled: "已安裝相容的瀏覽器",
	chromiumInstallFailed: "安裝 Microsoft Edge 失敗。請嘗試手動安裝: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "安裝 Microsoft Edge 逾時。請檢查您的網路連線。",
	browserAlreadyInstalled: "已安裝相容的瀏覽器",
	browserInstalled: "Microsoft Edge 已成功安裝！",
	browserInstallFailed: "安裝 Microsoft Edge 失敗。請嘗試手動安裝: flatpak install com.microsoft.Edge",
	flatpakNotFound: "系統未偵測到 Flatpak 環境",
	signInRequired: "需要登入以進行遊玩",
	syncSkippedNoSubscription: "未偵測到有效的 Xbox Game Pass 會員資格。xCloud 遊戲庫同步已被跳過。",
	syncSkippedTierUnknown: "偵測到 Xbox Game Pass 訂閱，但未能確認會員級別。為了避免顯示無法執行的遊戲，已跳過同步。",
	subscriptionCheckFailed: "無法驗證 Xbox Game Pass 訂閱狀態。已跳過 xCloud 同步。請在網路連線穩定時重試。",
	subscriptionDetected: "已偵測到有效的 Xbox Game Pass {{tier}} 訂閱。"
};
var playButton$1 = {
	play: "執行",
	install: "安裝",
	cancel: "取消",
	retry: "重試",
	downloadFailed: "下載失敗",
	installing: "安裝中",
	controllerConfig: "控制器配置",
	appSettings: "應用設定"
};
var play$1 = {
	play: "開始遊玩",
	resume: "繼續",
	stop: "停止",
	cancel: "取消",
	cancelling: "正在取消…",
	update: "更新",
	uninstall: "解除安裝",
	queued: "佇列中",
	downloading: "下載中",
	extracting: "解壓縮中",
	verifying: "正在驗證",
	finalizing: "正在完成安裝",
	cancelConfirmTitle: "取消下載？",
	cancelConfirmBody: "您確定要取消安裝 {{title}} 嗎？未完成下載的檔案將被刪除。",
	cancelConfirmConfirm: "取消下載",
	cancelConfirmCancel: "繼續下載",
	playOnCloud: "雲端遊玩",
	cloudSave: {
		autoPullOn: "啟動時自動下載雲端存檔。",
		close: "關閉",
		download: "下載雲端存檔",
		filesCount: "{{count}} 個檔案",
		label: "雲端存檔",
		menuTitle: "雲端存檔 — {{game}}",
		noCloudData: "未知",
		noCloudSupport: "不支援雲端存檔",
		setLocation: "設定存檔位置",
		setLocationHint: "選擇此遊戲儲存其存檔的資料夾。",
		setLocationTitle: "選擇此遊戲的存檔資料夾",
		statusCloudAvailable: "雲端存檔可用",
		statusNoSaves: "無本機存檔",
		statusSyncing: "正在同步雲端存檔…",
		statusUnresolved: "未找到存檔位置 — 此遊戲可能無法進行同步。",
		unresolvedHint: "啟動遊戲一次以便我們檢測其存檔資料夾，或手動設定位置。",
		upload: "上傳本機存檔"
	},
	exe: {
		browse: "瀏覽…",
		changeFailed: "無法變更可執行檔",
		changed: "可執行檔已變更",
		defaultTag: "(預設)",
		menuItem: "變更可執行檔…",
		reset: "重設為預設可執行檔",
		resetButton: "重設為預設",
		subtitle: "選擇此遊戲啟動的檔案（例如：跳過啟動器並直接執行遊戲）。工作目錄和雲端存檔不受影響。",
		title: "變更可執行檔 — {{game}}",
		unavailable: "無法讀取此遊戲的資料夾。"
	}
};
var playMeta$1 = {
	spaceRequired: "所需空間",
	installedSize: "佔用空間",
	lastPlayed: "上次執行",
	neverPlayed: "從未執行過",
	hoursShort: "小時",
	minutesShort: "分鐘",
	played: "已玩"
};
var downloads$1 = {
	title: "下載",
	empty: "沒有正在進行的下載。從您的庫中安裝遊戲即可在此處查看。",
	current: "正在下載",
	queued: "排隊佇列",
	finished: "最近完成",
	cancel: "取消",
	removeFromQueue: "移出佇列",
	play: "遊玩",
	outcome: {
		queued: "佇列中",
		downloading: "下載中",
		completed: "已完成",
		cancelled: "已取消",
		error: "錯誤"
	}
};
var navigation$1 = {
	loading: "正在載入...",
	storesTitle: "商店連接",
	syncTitle: "同步遊戲庫",
	languageTitle: "語言設定"
};
var security$1 = {
	panelTitle: "安全安全",
	counters: {
		label: "計數器",
		empty: "自啟動以來未偵測到安全事件"
	},
	log: {
		label: "審計日誌",
		entryCount: "{{count}} 個記錄",
		entryCount_other: "{{count}} 個記錄",
		emptyFiltered: "沒有與目前過濾相符的日誌事件"
	},
	filter: {
		label: "過濾",
		all: "全部事件"
	},
	timeline: {
		label: "24小時安全事件走勢 ({{count}} 個事件)",
		label_other: "24小時安全事件走势 ({{count}} 個事件)",
		axisStart: "-24小時",
		axisEnd: "目前"
	},
	relativeTime: {
		seconds: "{{count}} 秒前",
		minutes: "{{count}} 分鐘前",
		hours: "{{count}} 小時前",
		days: "{{count}} 天前"
	},
	bruteforce: {
		warningTitle: "警告：偵測到暴力破解行為 (警告級別)",
		escalationTitle: "警報：偵測到暴力破解行為 (嚴重等級)",
		detail: "在過去的 {{window}} 秒內，解密失敗達到了 {{failures}} 次。閾值設定：{{warning}} (警告) / {{escalation}} (警報)。",
		resetButton: "確認並重設偵測器"
	},
	actions: {
		clearLog: "清空日誌與計數器"
	},
	events: {
		tokenEncrypted: "憑據已加密",
		tokenDecrypted: "憑據已解密",
		decryptFailed: "解密失敗",
		tokenFileMigrated: "憑據檔案已遷移",
		legacyPlaintextDetected: "偵測到舊版明文資料",
		authFlowStarted: "已啟動授權流程",
		authFlowCompleted: "已完成授權流程",
		authFlowFailed: "授權流程失敗",
		permissionsCheck: "權限檢查完成",
		permissionsRepaired: "權限已成功修復",
		bruteforceSuspected: "疑似暴力破解行為",
		deviceResetDetected: "偵測到裝置重設事件",
		fingerprintInitialized: "指紋裝置已初始化",
		externalAuthCheckFailed: "外部連通認證確認失敗",
		configValidationCompleted: "配置項有效性核對完成",
		configValidationFailed: "配置項有效性核对失敗"
	},
	configValidation: {
		degradedTitle: "配置錯誤：功能受限模式執行中",
		detail: "啟動階段確認了 {{count}} 個配置異常",
		detail_other: "啟動階段確認了 {{count}} 個配置異常",
		sourceDefaults: "預設選項",
		sourceUser: "使用者設定",
		sourceMerged: "已合併項",
		overflow: "+ 另有 {{count}} 個錯誤",
		overflow_other: "+ 另有 {{count}} 個错误"
	}
};
var cloudSyncBehavior$1 = {
	_comment: "配置面板，供使用者針對每一個商店配置雲端存檔同步錯誤展示策略。其中的 5 項 store_label 與 CloudSyncBehaviorPanel.tsx 的行渲染相對應。Microsoft 已略去，因為 xCloud 不需要同步本機檔案。",
	title: "雲端存檔同步行為",
	description: "設定當遊戲執行前下載雲存檔、或遊戲退出後上傳雲存檔發生問題時，Unifideck 的回饋方案。",
	loadError: "無法載入目前配置",
	saveError: "儲存配置失敗，請重新嘗試",
	store_label: {
		"default": "預設設定 (全部商店)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "靜默 (僅記入日誌檔案)",
		toast: "彈出提示通知"
	}
};
var cloudSync$1 = {
	_comment: "同步出現錯誤時前端呈現 of 提示。後端拋出 of error_i18n_key 決定對應到哪一個 cloudSync.error.<code> 上。",
	error: {
		network_unreachable: "無法連線到網路",
		auth_expired: "授權已失效，請重登帳號",
		forbidden: "請求被拒絕 (無權限)",
		quota_exceeded: "雲存檔儲存容量已超限",
		server_error: "商店雲端伺服器異常",
		timed_out: "網路通訊超時",
		disk_full: "磁碟空間不足",
		disk_space_low: "已跳過：可用磁碟空間偏低 (請釋放空間後再試)",
		permission_denied: "權限被拒絕",
		cancelled: "操作已取消",
		unknown: "未預料的系統錯誤"
	},
	unavailableNativeDep: "此系統上的雲端存檔已關閉 — 其 Python ({{python}}) 缺少必要的組件。您的本機存檔不受影響；請在系統更新後重新構建/重新安裝插件以恢復同步。"
};
var launchLogs$1 = {
	_comment: "LaunchLogsModal 使用 of 國際化文案。彈窗通過 launcher 出錯通知中附加 of unifideck://show-logs/<launch_id> 協定喚起。它用於讀取儲存在 ~/.local/share/unifideck/launches/<launch_id>.log 中的單次運行日誌，並提供帶等級色彩的分類渲染展示。",
	title: "啟動日誌查詢 — {{launchId}}",
	close: "關閉",
	copyAll: "全部複製",
	"export": "匯出到檔案",
	copied: "已成功複製到剪貼簿",
	copyFailed: "複製操作失敗",
	exporting: "正在匯出…",
	exportedTo: "已成功匯出至 {{path}}",
	exportFailed: "檔案匯出出錯: {{error}}",
	loading: "正在載入執行日誌…",
	notAvailable: "本次啟動的日誌資料已不復存在 (已超 7 天的日常儲存保留期限)。",
	error: "未能讀取日誌內容: {{error}}",
	fileInfo: "{{path}} — 目前顯示 {{shown}} 行，總計 {{total}} 行",
	filterLabel: "按級別顯示",
	filterInfo: "資訊",
	filterWarning: "警告",
	filterError: "異常",
	nextError: "定位至下個異常 ({{current}}/{{total}})",
	noErrors: "未檢索到錯誤資訊",
	allFiltered: "因目前過濾規則，所有日誌行已全部被隱藏。請至少勾選一個日誌等級以查看內容。"
};
var settings$1 = {
	_comment: "UnifideckSettingsPanel 基礎操控。其中的 refreshAll 用於一次性觸發所有啟用商店的背景同步任務 (sync_all)。遊戲庫管理面板會關注 SYNC_PROGRESS 推送來即時展現目前進度。",
	storeConnections: "綁定商店帳號",
	librarySync: "同步遊戲庫",
	language: "語言",
	languageAuto: "自適應系統語言",
	refreshAll: {
		sectionTitle: "遊戲庫資料操作",
		button: "同步全部已連結的遊戲庫",
		buttonBusy: "正在啟動背景同步…",
		startedTitle: "同步指令發送成功",
		startedBody: "所有商店的資料同步任務目前均在背景靜默執行中。",
		startedSingle: "{{store}} 商店的資料同步目前正在背景進行中。",
		failedTitle: "同步指令派發失敗"
	}
};
var saveFolder$1 = {
	_comment: "SaveFolderModal of 提示資訊。通常在雲同步失效且玩家試圖手動定位/查看目前本地有哪些可用文件時，經由 unifideck://open-save-folder/<store>/<game_id> 協定調起該彈框。count 和 size 會插值；truncated 在文件數目觸發上限而僅顯示部分列表時啟用。",
	title: "查看本地存檔 — {{store}} / {{gameId}}",
	close: "關閉",
	copyPath: "複製資料夾路徑",
	copyFileList: "複製文件明細列表",
	depthLabel: "目錄深度",
	filterLabel: "關鍵字查找",
	loading: "正在讀取本地目錄資訊…",
	noSavesYet: "未找到本地儲存的存檔內容。首次進行雲存檔成功同步後系統將為您建立相應資料夾。",
	aggregate: "總共包含 {{count}} 個文件，合併佔用 {{size}}",
	truncated: "另有 {{count}} 個文件 (佔用 {{size}}) 未能在下表中完全呈現，請縮窄篩選條件。",
	error: "本地存檔讀取操作發生异常: {{error}}"
};
var library$1 = {
	_comment: "庫詳情中的針對每一個遊戲的管理功能，包含自動斷路判定提示，以及多次故障後提供的繞过測試與數據初始化清空選項。",
	circuitBreaker: {
		_comment: "錯誤熔斷 of 交互文案。僅當在特定的熔斷窗口中出錯次數觸發設定的 circuit_breaker.failures_threshold (預設值是 600 秒 3 次) 時提示該狀態。強行啟動允許暫時繞過；重置會將統計清零。",
		badgeRecent: "最近發生了 {{count}} 次執行失敗",
		badgeRecent_other: "最近發生了 {{count}} 次執行失敗",
		badgeTooltip: "由於近來執行失敗已達 {{count}} 次，自動熔斷機制已介入並暫停了本遊戲的自動化啟動流程。",
		forceLaunchButton: "強制執行 (忽略攔截)",
		resetButton: "清零錯誤次數",
		failureKind: {
			fastBoot: "啟動後秒退崩潰",
			launcherError: "商店用戶端配置異常"
		}
	}
};
var pickStorage$1 = {
	title: "安裝路徑選擇",
	customOption: "定位至其他路徑...",
	customLabel: "自定義路徑",
	change: "更改設置...",
	gameSize: "安裝預佔空間"
};
var restart$1 = {
	title: "同步完成！",
	body: "請重啟 Steam 以套用您的新遊戲和封面",
	confirm: "立即重啟",
	later: "稍後"
};
var updater$1 = {
	checkButton: "檢查更新",
	checkCompleteMessage: "已成功獲取最新版本資訊。",
	checkCompleteTitle: "更新檢查完成",
	checkFailedTitle: "檢查失敗",
	checkingButton: "檢查中...",
	currentTitle: "目前版本",
	downgradeButton: "降級至 v{{version}}",
	installFailedTitle: "安裝失敗",
	installTimeoutMessage: "Decky Loader 無回應 — 安裝可能已停滯或失敗。請重試。",
	installedLabel: "已安裝",
	installingButton: "正在安裝...",
	installingTitle: "安裝外掛程式中",
	latestLabel: "最新版本",
	modalTitle: "UNIFIDECK v{{version}} — 發行說明",
	noBackend: "Decky 後端服務不可用。",
	noReleaseNotes: "無可用發行說明。",
	reinstallButton: "重新安裝 v{{version}}",
	refreshFailedMessage: "無法驗證最新版本資訊。請檢查網路連線後重試。",
	releaseGoneMessage: "此版本已不再可用。請檢查更新後重新選擇。",
	releaseNotesButton: "發行說明",
	stageDownload: "下載中…",
	stageFinish: "正在完成…",
	stageInstall: "安裝中…",
	stageOpen: "讀取包中…",
	stageParse: "正在驗證…",
	stageRemove: "正在移除舊版本…",
	stageStart: "正在啟動…",
	stageWorking: "安裝中…",
	titleLoading: "正在檢查版本",
	typeDowngrade: "降級至",
	typeReinstall: "重新安裝",
	typeUpdate: "升級至",
	updateButton: "升級至 v{{version}}"
};
var achievements$1 = {
	unlockedTitle: "成就已解鎖",
	unlockedBody: "{{name}}"
};
var achievementsModal$1 = {
	title: "成就",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "重新整理",
	close: "關閉",
	loading: "正在載入成就…",
	empty: "此遊戲沒有成就。",
	hidden: "隱藏成就",
	unlockedOn: "{{date}} 解鎖",
	error: "無法載入成就。",
	errorOffline: "連接到網際網路以檢視成就。",
	errorAuth: "登入此遊戲的商店以檢視成就。",
	errorUnavailable: "此遊戲不支援成就。"
};
var collectionSettings$1 = {
	title: "STEAM 收藏",
	enabled: "已啟用收藏",
	disabled: "已禁用收藏"
};
var zhTW = {
	_comment: _comment$1,
	tabs: tabs$1,
	auth: auth$1,
	storage: storage$1,
	storageSettings: storageSettings$1,
	gameGrid: gameGrid$1,
	downloadsTab: downloadsTab$1,
	cloudSave: cloudSave$1,
	storeConnections: storeConnections$1,
	librarySync: librarySync$1,
	cleanup: cleanup$1,
	captureLogs: captureLogs$1,
	installButton: installButton$1,
	confirmModals: confirmModals$1,
	toasts: toasts$1,
	uninstallModal: uninstallModal$1,
	languageSettings: languageSettings$1,
	errors: errors$1,
	sync: sync$1,
	force_sync: force_sync$1,
	artwork: artwork$1,
	deckTabs: deckTabs$1,
	unifiedLibrary: unifiedLibrary$1,
	gameInfoPanel: gameInfoPanel$1,
	gameDetailsSettings: gameDetailsSettings$1,
	gogLanguageModal: gogLanguageModal$1,
	accountSwitch: accountSwitch$1,
	ubisoftAuth: ubisoftAuth$1,
	relativeDate: relativeDate$1,
	common: common$1,
	info: info$1,
	microsoft: microsoft$1,
	playButton: playButton$1,
	play: play$1,
	playMeta: playMeta$1,
	downloads: downloads$1,
	navigation: navigation$1,
	security: security$1,
	cloudSyncBehavior: cloudSyncBehavior$1,
	cloudSync: cloudSync$1,
	launchLogs: launchLogs$1,
	settings: settings$1,
	saveFolder: saveFolder$1,
	library: library$1,
	pickStorage: pickStorage$1,
	restart: restart$1,
	updater: updater$1,
	achievements: achievements$1,
	achievementsModal: achievementsModal$1,
	collectionSettings: collectionSettings$1
};

var _comment = "Unified catalog: merged from legacy src893/staging (28 top-level sections, 516 lines) + architectural-refactor additions (cloudSync, cloudSyncBehavior, launchLogs, library, navigation, playButton, saveFolder, security, settings, + new launcher toasts). The overlay policy is current-wins-on-conflict so new code's expected labels survive. Legacy top-level sections (accountSwitch, storageSettings, ubisoftAuth, etc.) are preserved as-is since their consumer components migrate unchanged.";
var tabs = {
	settings: "الإعدادات",
	downloads: "التنزيلات"
};
var auth = {
	connect: "ربط",
	disconnect: "إلغاء الربط",
	status: {
		connected: "متصل",
		disconnected: "غير متصل",
		checking: "جاري التحقق…",
		error: "خطأ — تحقق من السجلات"
	},
	errors: {
		networkTitle: "No internet connection",
		networkBody: "Connect to Wi-Fi and try again."
	}
};
var storage = {
	title: "وحدة التخزين",
	defaultUpdated: "تم تحديث وحدة التخزين الافتراضية",
	isDefault: "الافتراضي",
	setDefault: "تعيين كافتراضي",
	customPath: "مسار مخصص",
	up: "↑ للأعلى",
	refresh: "تحديث",
	useThisPath: "استخدام هذا المسار"
};
var storageSettings = {
	title: "إعدادات التنزيل",
	installLocationLabel: "موقع التثبيت",
	installLocationDescription: "أين سيتم تنزيل الألعاب الجديدة",
	pathLabel: "المسار",
	loading: "جاري تحميل خيارات التخزين...",
	toastUpdatedTitle: "تم تحديث موقع التثبيت",
	toastUpdatedBody: "سيتم تثبيت الألعاب الجديدة في {{location}}",
	toastFailedTitle: "فشل التحديث",
	toastFailedBody: "{{error}}",
	unknown: "غير معروف",
	browseButton: "تصفح...",
	clearCustom: "مسح الموقع المخصص",
	customPathSet: "تم تعيين موقع تثبيت مخصص",
	newFolder: "مجلد جديد",
	newFolderCreate: "إنشاء",
	selectFolder: "اختر هذا المجلد",
	emptyDirectory: "مجلد فارغ",
	showHidden: "إظهار الملفات المخفية",
	selectDevice: "اختر جهاز التخزين",
	sortAZ: "أ-ي",
	sortZA: "ي-أ",
	sortModifiedNewest: "تاريخ التعديل (الأحدث أولاً)",
	sortModifiedOldest: "تاريخ التعديل (الأقدم أولاً)",
	sortCreatedNewest: "تاريخ الإنشاء (الأحدث أولاً)",
	sortCreatedOldest: "تاريخ الإنشاء (الأقدم أولاً)",
	sortSizeLargest: "الحجم (الأكبر أولاً)",
	sortSizeSmallest: "الحجم (الأصغر أولاً)",
	"validationErrors.notAbsolute": "يجب أن يكون المسار مطلقاً",
	"validationErrors.systemDir": "لا يمكن استخدام مجلد النظام",
	"validationErrors.notWritable": "الموقع غير قابل للكتابة",
	"validationErrors.notFound": "المجلد غير موجود",
	"validationErrors.homeRoot": "لا يمكن استخدام مجلد المستخدم الرئيسي كجذر",
	freeSpaceLabel: "{{label}} (المساحة المتبقية {{freeSpace}} جيجابايت)"
};
var gameGrid = {
	loading: "جاري تحميل الألعاب...",
	noGamesFound: "لم يتم العثور على ألعاب",
	noGamesHint: "حاول مزامنة مكتباتك من إعدادات Unifideck",
	showingGames: "عرض {{count}} من الألعاب",
	installed: "مثبتة",
	notInstalled: "غير مثبتة",
	playtime: "وقت اللعب: {{hours}} ساعة و {{minutes}} دقيقة"
};
var downloadsTab = {
	currentDownload: "التنزيل الحالي",
	queuedDownloads: "في الانتظار ({{count}})",
	recentlyCompleted: "اكتملت مؤخراً",
	noActiveDownloads: "لا توجد تنزيلات نشطة",
	noDownloads: "لا توجد تنزيلات. قم بتثبيت ألعاب من مكتبتك لتراها هنا.",
	loadingLabel: "جاري التحميل...",
	loadingMessage: "جاري جلب قائمة الانتظار للتحميل...",
	confirmCancelTitle: "تأكيد الإلغاء",
	confirmCancelDescription: "هل أنت متأكد من إلغاء تنزيل {{game}}؟",
	confirmCancelYes: "نعم",
	confirmCancelNo: "لا",
	cancelDownload: "إلغاء",
	toastDownloadCancelledTitle: "تم إلغاء التنزيل",
	toastDownloadCancelledBody: "تمت إزالة التنزيل من قائمة الانتظار.",
	toastCancelFailedTitle: "فشل الإلغاء",
	toastCancelFailedBody: "تعذر إلغاء التنزيل: {{error}}",
	status: {
		queued: "في الانتظار",
		downloading: "جاري التنزيل",
		completed: "اكتمل",
		cancelled: "ملغى",
		error: "خطأ"
	},
	preparingDownload: "جاري تحضير التنزيل...",
	phaseExtracting: "جاري استخراج ملفات {{game}}...",
	phaseVerifying: "جاري التحقق من {{game}}...",
	internalStorage: "وحدة التخزين الداخلية (المساحة المتبقية {{freeSpace}} جيجابايت)",
	sdCard: "بطاقة SD (المساحة المتبقية {{freeSpace}} جيجابايت)",
	customLocation: "مخصص: {{path}} (المساحة المتبقية {{freeSpace}} جيجابايت)",
	extractingLabel: "جاري الاستخراج...",
	verifyingLabel: "جاري التحقق...",
	updateQueuedLabel: "تحديث في قائمة الانتظار",
	downloadQueuedLabel: "تنزيل في قائمة الانتظار",
	downloadingUpdateLabel: "جاري تنزيل التحديث",
	downloadingLabel: "جاري التنزيل...",
	finalizingInstallation: "جاري إنهاء التثبيت...",
	speedMbps: "{{speed}} ميجابايت/ثانية",
	etaLabel: "الوقت المتبقي: {{eta}}",
	preparingMessage: "جاري تهيئة البادئة ومزامنة الحفظ السحابي...",
	preparingLabel: "جارٍ إعداد اللعبة...",
	installingViaUpcLabel: "التثبيت عبر Ubisoft Connect",
	extractingMessage: "جارٍ استخراج الملفات...",
	verifyingMessage: "جارٍ التحقق من الملفات... {{pct}}%",
	installingViaUpcMessage: "اتبع نافذة Ubisoft Connect لإكمال التثبيت..."
};
var cloudSave = {
	title: "تعارض الحفظ السحابي",
	description: "تختلف بيانات الحفظ المحلية للعبة {{game}} عن الحفظ السحابي",
	local: "المحلي:",
	cloud: "السحابي:",
	newer: "(الأحدث)",
	useCloud: "استخدام الحفظ السحابي",
	useLocal: "استخدام الحفظ المحلي",
	unknownDate: "غير معروف",
	toastUsingCloudTitle: "استخدام الحفظ السحابي",
	toastUsingCloudBody: "جاري تنزيل الحفظ السحابي لـ {{game}}",
	toastUsingLocalTitle: "استخدام الحفظ المحلي",
	toastUsingLocalBody: "جاري رفع الحفظ المحلي لـ {{game}} إلى السحابة",
	toastErrorTitle: "خطأ",
	toastErrorBody: "تعذر حل تعارض الحفظ",
	filesSize: "{{count}} ملفات · {{size}}",
	uploadBlockedEmpty: "تم تخطّي الرفع السحابي لـ {{game}} — لم يتم العثور على حفظ محلي",
	uploadSkippedTitle: "تم تخطّي الرفع السحابي",
	uploadSkippedBody: "لا توجد حفظ محلي للرفع بعد لـ {{game}}."
};
var storeConnections = {
	title: "ربط المتاجر",
	epicGames: "Epic Games",
	gog: "GOG",
	amazonGames: "Amazon Games",
	ubisoftConnect: "Ubisoft Connect",
	microsoftStore: "Microsoft Store",
	connected: "متصل",
	checking: "جاري التحقق...",
	authenticate: "تسجيل الدخول",
	legendaryNotInstalled: "⚠️ جاري تثبيت أداة Epic Games...",
	nileNotInstalled: "⚠️ أداة Amazon Games غير موجودة",
	error: "خطأ — تحقق من السجلات"
};
var librarySync = {
	title: "مزامنة المكتبة",
	syncLibraries: "مزامنة المكتبات",
	forceSync: "مزامنة إجبارية",
	syncing: "جاري المزامنة...",
	cancelSync: "إلغاء المزامنة",
	retryStatusCheck: "إعادة محاولة التحقق من الحالة",
	gamesSynced: "تمت مزامنة الألعاب {{synced}} / {{total}}",
	artworkDownloaded: "تم تنزيل الأغلفة {{synced}} / {{total}}",
	metadataExtracted: "تم استخراج البيانات الوصفية {{synced}} / {{total}}",
	steamMetadataDownloaded: "البيانات الوصفية لـ Steam: {{synced}} / {{total}}",
	unifidbMetadataDownloaded: "البيانات الوصفية لـ unifiDB: {{synced}} / {{total}}",
	metacriticMetadataDownloaded: "بيانات Metacritic: {{synced}} / {{total}}",
	rawgMetadataDownloaded: "البيانات الوصفية لـ RAWG: {{synced}} / {{total}}",
	compatChecked: "بيانات التوافق: {{synced}} / {{total}}",
	cancelling: "جاري الإلغاء…",
	error: "خطأ"
};
var cleanup = {
	title: "تنظيف البيانات",
	deleteAll: "حذف جميع مكتبات وكاش UNIFIDECK",
	modalTitle: "حذف جميع بيانات Unifideck؟",
	modalDescription: "سيؤدي هذا إلى إزالة جميع اختصارات Unifideck، وأغلفة Steam، وبيانات تسجيل الدخول المحفوظة للمتاجر، وبيانات الكاش. ستبقى الألعاب المثبتة على القرص ويمكن مزامنتها لاحقاً. قم بتفعيل الوضع التدميري أدناه لحذف ملفات الألعاب المثبتة أيضاً.",
	deleteFilesLabel: "حذف ملفات الألعاب المثبتة أيضاً (تدميري)",
	deleteFilesDescription: "يحذف دليل التثبيت على القرص لكل لعبة Unifideck تحت المجلد الجذري الآمن.",
	confirmDelete: "نعم، احذف الكل",
	cancel: "إلغاء",
	deleting: "جاري الحذف..."
};
var captureLogs = {
	_comment: "CaptureLogsSection + CaptureLogsConfirmModal (ar-SA).",
	title: "جمع السجلات",
	description: "يجمع كل سجلات Unifideck في ملف مضغوط واحد في مجلد التنزيلات، لتتمكن من إرفاقه بتقرير خطأ.",
	capture: "جمع السجلات",
	capturing: "جارٍ جمع السجلات...",
	savedTo: "تم الحفظ في مجلد التنزيلات",
	bundleInfo: "{{files}} ملفات · {{size}}",
	partialNote: "تعذّر قراءة {{count}} ملف. الحزمة قابلة للاستخدام مع ذلك.",
	partialNote_other: "تعذّر قراءة {{count}} ملفات. الحزمة قابلة للاستخدام مع ذلك.",
	copyPath: "نسخ المسار",
	modalTitle: "إنشاء حزمة تشخيص؟",
	modalDescription: "سيجمع Unifideck سجلاته في ملف مضغوط واحد في مجلد التنزيلات، لتتمكن من إرفاقه بتقرير خطأ.",
	modalIncludes: "يشمل: سجلات الإضافة وتشغيل الألعاب، وسجل المزامنة والتثبيت، وأسماء ألعابك ومسارات تثبيتها، ومعلومات عن جهازك مثل الطراز ونظام التشغيل ووحدات التخزين.",
	modalExcludes: "لا يشمل أبداً: كلمات المرور، أو رموز تسجيل الدخول إلى المتاجر، أو ملفات تعريف الارتباط.",
	modalConfirm: "إنشاء الحزمة",
	cancel: "إلغاء"
};
var installButton = {
	play: "تشغيل",
	resume: "استئناف",
	install: "تثبيت {{title}}",
	installNative: "تثبيت",
	uninstall: "إلغاء تثبيت {{title}}",
	cancel: "✖ إلغاء",
	processing: "جاري المعالجة...",
	checking: "جاري التحقق...",
	size: "{{size}} جيجابايت",
	update: "تحديث",
	updateAvailable: "يتوفر تحديث",
	updateAvailableDescription: "نسخة جديدة جاهزة للتثبيت."
};
var confirmModals = {
	installTitle: "تأكيد التثبيت",
	installDescription: "هل أنت متأكد من رغبتك في تثبيت {{title}}؟",
	uninstallTitle: "تأكيد إلغاء التثبيت",
	uninstallDescription: "هل أنت متأكد من رغبتك في إلغاء تثبيت {{title}}؟",
	cancelTitle: "تأكيد الإلغاء",
	cancelDescription: "هل أنت متأكد من رغبتك في إلغاء تنزيل {{title}}؟",
	forceSyncTitle: "إعدادات المزامنة الإجبارية",
	forceSyncDescription: "هل ترغب في إعادة مزامنة جميع الأغلفة؟ سيؤدي ذلك إلى استبدال أي تغييرات يدوية قمت بها على الأغلفة. اختر 'الاحتفاظ بالأغلفة' لتنزيل الأغلفة المفقودة فقط.",
	resyncArtwork: "إعادة مزامنة الأغلفة",
	keepArtwork: "الاحتفاظ بالأغلفة",
	steamRestartTitle: "اكتملت المزامنة!",
	steamRestartDescription: "يرجى إعادة تشغيل Steam لتطبيق الألعاب والأغلفة الجديدة",
	steamRestartDescriptionStore: "يرجى إعادة تشغيل Steam لتطبيق ألعاب وأغلفة {{store}} الجديدة",
	steamRestartCleanupTitle: "اكتمل التنظيف!",
	steamRestartCleanupDescription: "يرجى إعادة تشغيل Steam لإزالة الألعاب والأغلفة المحذوفة بالكامل",
	restartNow: "إعادة التشغيل الآن",
	later: "لاحقاً",
	ubisoftInstallTitle: "Ubisoft Connect مطلوب",
	ubisoftInstallDescription: "سيؤدي هذا إلى تشغيل برنامج تثبيت Ubisoft Connect المطلوب لتشغيل {{title}}.",
	ubisoftInstallConfirm: "فتح Ubisoft Connect",
	yes: "نعم",
	no: "لا",
	amazonUpdateTitle: "يتطلب إعادة تنزيل كاملة",
	amazonUpdateDescription: "تتطلب تحديثات Amazon إعادة تنزيل لعبة {{title}} بالكامل. قد يستغرق هذا بعض الوقت. هل ترغب في الاستمرار؟",
	updateConfirm: "تحديث"
};
var toasts = {
	downloadStarted: "بدأ التنزيل. تحقق من UNIFIDECK > التنزيلات",
	downloadQueued: "التنزيل في الانتظار",
	downloadQueuedBody: "تمت إضافة {{title}} إلى قائمة الانتظار.",
	downloadFailedUnknown: "خطأ غير معروف.",
	updateQueued: "التحديث في قائمة الانتظار",
	uninstallDone: "اكتمل إلغاء التثبيت.",
	ubisoftLauncherOpening: "جاري فتح Ubisoft Connect",
	ubisoftLauncherOpeningMessage: "جاري تشغيل Ubisoft Connect لـ {{title}}. يرجى تثبيت اللعبة هناك ثم العودة للعب.",
	ubisoftLauncherAlreadyOpenMessage: "Ubisoft Connect مفتوح بالفعل لهذا التثبيت.",
	ubisoftLauncherOpenFailed: "فشل فتح Ubisoft Connect",
	ubisoftLauncherOpenFailedMessage: "فشل تشغيل Ubisoft Connect للتثبيت.",
	multipartDetected: "تم اكتشاف تنزيل متعدد الأجزاء",
	multipartMessage: "يرجى الانتظار لحين اكتمال العملية",
	downloadFailed: "فشل التنزيل",
	downloadFailedMessage: "تعذر بدء التنزيل.",
	downloadCancelled: "تم إلغاء التنزيل",
	downloadCancelledMessage: "تم إلغاء تنزيل {{title}}.",
	cancelFailed: "فشل الإلغاء",
	cancelFailedMessage: "تعذر إلغاء التنزيل.",
	uninstalling: "جاري إلغاء التثبيت...",
	uninstallingMessage: "جاري إلغاء تثبيت {{title}}...",
	uninstallingMessageProton: "جاري إلغاء تثبيت {{title}} وملفات Proton...",
	uninstallComplete: "اكتمل إلغاء التثبيت",
	uninstallCompleteMessage: "تمت إزالة لعبة {{title}}.",
	uninstallCompleteMessageProton: "تمت إزالة لعبة {{title}} (بما في ذلك ملفات Proton).",
	installComplete: "اكتمل التثبيت!",
	installCompleteMessage: "اللعبة {{title}} جاهزة للتشغيل.",
	syncComplete: "اكتملت المزامنة!",
	syncCompleteMessage: "تمت إضافة {{count}} ألعاب. يرجى إعادة تشغيل STEAM (الخروج تماماً من التطبيق وليس فقط العودة إلى وضع اللعب) لرؤيتها في مكتبتك.",
	forceSyncComplete: "اكتملت المزامنة الإجبارية!",
	forceSyncCompleteMessage: "تم تحديث {{count}} ألعاب. يرجى إعادة تشغيل STEAM لتطبيق التغييرات.",
	syncCancelled: "تم إلغاء المزامنة",
	authTimeout: "انتهت مهلة المصادقة",
	authTimeoutMessage: "انتهت مهلة المصادقة مع {{store}}. يرجى المحاولة مرة أخرى.",
	authFailed: "فشلت المصادقة",
	authFailedMessage: "تعذر بدء عملية المصادقة",
	authError: "خطأ في المصادقة",
	authConnected: "تم ربط متجر {{store}}!",
	authConnectedMessage: "تمت المصادقة بنجاح مع {{store}}",
	cleanupStarted: "بدأ تنظيف البيانات",
	cleanupStartedMessage: "جاري إزالة الاختصارات والأغلفة وبيانات تسجيل الدخول المحفوظة. قد يستغرق هذا بعض الوقت…",
	cleanupSuccessful: "اكتمل التنظيف بنجاح",
	cleanupSuccessfulMessage: "تم حذف {{games}} من الاختصارات، و {{artwork}} من ملفات الأغلفة، وتم تسجيل الخروج من {{stores}} من المتاجر، وحذف {{files}} من مجلدات التثبيت. تم تفريغ الكاش.",
	cleanupNoop: "لا يوجد شيء لتنظيفه",
	cleanupNoopMessage: "لا توجد اختصارات أو أغلفة أو بيانات تسجيل دخول محفوظة لإزالتها.",
	deleteFailed: "فشل الحذف",
	repairPrefixSuccess: "تم إصلاح بيئة التشغيل",
	repairPrefixSuccessMessage: "تم إعادة بناء بيئة تشغيل Ubisoft Connect بنجاح",
	repairPrefixFailed: "فشل الإصلاح",
	repairPrefixFailedMessage: "فشل إصلاح بيئة التشغيل: {{error}}",
	unifideck: "Unifideck:",
	launcher: {
		networkError: "خطأ في الشبكة",
		launchRetry: "إعادة محاولة التشغيل",
		retryingUmu: "إعادة محاولة تشغيل UMU خلال {{seconds}} ثوانٍ (محاولة {{attempt}}/{{max}})...",
		protonUpgrade: "تحديث Proton",
		resettingPrefix: "جاري إعادة تعيين بيئة التشغيل لـ {{version}}...",
		firstTimeSetup: "الإعداد لأول مرة",
		initializingPrefix: "جاري تهيئة بيئة تشغيل Wine...",
		setupCompleteTitle: "اكتمل الإعداد",
		prefixInitialized: "تمت تهيئة بيئة تشغيل Wine",
		setupFallback: "الإعداد",
		fallbackInitialization: "جاري تشغيل التهيئة الاحتياطية...",
		launchingGame: "جاري تشغيل اللعبة",
		startingEpicGame: "جاري تشغيل لعبة Epic...",
		startingAmazonGame: "جاري تشغيل لعبة Amazon...",
		startingGogGame: "جاري تشغيل لعبة GOG...",
		startingUbisoftGame: "جاري تشغيل لعبة Ubisoft...",
		dependenciesTitle: "تثبيت التبعيات",
		installingRedistributables: "جاري تثبيت البرامج المساعدة للعبة...",
		dependenciesReady: "التبعيات جاهزة",
		redistributablesInstalled: "تم تثبيت البرامج المساعدة للعبة",
		dependenciesStatus: "حالة التبعيات",
		checkLogs: "اكتمل الإعداد (راجع السجلات لمزيد من التفاصيل)",
		installingProton: "جاري تثبيت Proton",
		attemptingInstall: "جاري محاولة تثبيت {{version}}...",
		epicAuthRequired: "يتطلب تسجيل الدخول إلى Epic",
		epicAuthRequiredMessage: "يرجى تسجيل الدخول إلى Epic Games في إعدادات Unifideck",
		installingRedist: "تثبيت المكتبات المشتركة",
		installingRedistMessage: "تشغيل التثبيت الأولي لمكتبات GOG...",
		setupFailed: "فشل الإعداد",
		setupFailedMessage: "فشل التثبيت، يرجى مراجعة السجلات لمزيد من التفاصيل",
		prerequisitesTitle: "تثبيت المكونات الضرورية",
		installingPrerequisites: "جاري تثبيت المكونات المطلوبة لتشغيل اللعبة...",
		prerequisitesReady: "المكونات جاهزة",
		prerequisitesInstalled: "تم تثبيت مكونات اللعبة الضرورية بنجاح",
		prerequisitesFailed: "تحذير المكونات الضرورية",
		prerequisitesFailedMessage: "قد لا تكون بعض مكونات اللعبة قد تثبتت بشكل صحيح",
		browserRequired: "يتطلب Microsoft Edge",
		signingIn: "جاري تسجيل الدخول...",
		cloudSyncDownFailed: "فشل تنزيل ملفات الحفظ السحابي لـ {{store}} ({{error}}). تم تشغيل اللعبة ببيانات الحفظ المحلية.",
		cloudSyncUpFailed: "فشل رفع ملفات الحفظ السحابي لـ {{store}} ({{error}}). تقدمك محفوظ محلياً فقط.",
		errorCircuitBreakerOpen: "فشل تشغيل {{game_key}} لـ {{count}} مرات متتالية مؤخراً. تم تعليق التشغيل التلقائي.",
		launcherError: "تعذر تشغيل {{game_key}} ({{error_code}}). راجع السجلات للتفاصيل.",
		downloadingProton: "جارٍ تنزيل Proton — قد يستغرق ذلك بضع دقائق...",
		downloadingRuntime: "جارٍ تنزيل Steam Runtime — قد يستغرق التشغيل الأول بضع دقائق...",
		installingUbisoft: "التثبيت عبر Ubisoft Connect",
		installingUbisoftMessage: "جارٍ فتح Ubisoft Connect — ابدأ التثبيت من نافذة المشغّل.",
		protonReadyBody: "{{version}} جاهز للاستخدام",
		protonReadyTitle: "Proton جاهز",
		protonSwitchedTo: "يتم الآن استخدام {{version}}",
		signingInUbisoft: "جارٍ تسجيل الدخول إلى Ubisoft Connect",
		signingInUbisoftMessage: "جارٍ فتح Ubisoft Connect — سجّل الدخول هناك ثم عُد.",
		ubisoftPrefixNotReady: "Ubisoft Connect غير جاهز",
		ubisoftPrefixNotReadyMessage: "تعذّر تجهيز بادئة Ubisoft Connect لهذه اللعبة. حاول التثبيت مرة أخرى؛ وإذا استمر الفشل، فأعد تشغيل مزامنة المكتبة.",
		ubisoftAuthPrefixNotReady: "Ubisoft Connect غير جاهز",
		ubisoftAuthPrefixNotReadyMessage: "تعذّر تجهيز Ubisoft Connect لتسجيل الدخول. يُرجى المحاولة مرة أخرى بعد قليل.",
		ubisoftLaunchIdMissing: "جارٍ فتح Ubisoft Connect",
		ubisoftLaunchIdMissingMessage: "تعذّر تحديد معرّف تشغيل هذه اللعبة — جارٍ فتح Ubisoft Connect لتتمكن من تشغيلها من مكتبتك. أعد تشغيل مزامنة المكتبة لإصلاح التشغيل المباشر."
	},
	updateComplete: "اكتمل التحديث!",
	updateCompleteMessage: "تم تحديث {{title}}.",
	updateCancelled: "تم إلغاء التحديث",
	updateCancelledMessage: "تم إلغاء تحديث {{title}}.",
	updateFailed: "فشل التحديث",
	updateFailedMessage: "تعذر بدء عملية التحديث.",
	storeError: "تعذر الاتصال بـ {{store}} ({{errType}})",
	captureLogsStarted: "جارٍ جمع السجلات...",
	captureLogsStartedMessage: "يجري إنشاء حزمة تشخيص في مجلد التنزيلات. يستغرق ذلك بضع ثوانٍ.",
	captureLogsDone: "تم جمع السجلات",
	captureLogsDoneMessage: "تم الحفظ في {{path}} ({{size}}، {{files}} ملفات).",
	captureLogsPartial: "تم جمع السجلات (جزئياً)",
	captureLogsPartialMessage: "تم الحفظ في {{path}} ({{size}}، {{files}} ملفات). تعذّر قراءة {{failures}} ملف.",
	captureLogsFailed: "تعذّر جمع السجلات",
	captureLogsBusy: "هناك عملية جمع سجلات جارية بالفعل",
	captureLogsCopied: "تم نسخ المسار",
	captureLogsCopyFailed: "تعذّر نسخ المسار",
	actions: {
		_comment: "Action button labels rendered next to toasts when the backend payload includes an ``action`` field. Each one is a one-click remediation for a specific error code. Steam URLs use SteamClient.URL.ExecuteSteamURL; unifideck:// URLs are dispatched through the dispatch_unifideck_action RPC (backend verbs) or handled locally (frontend verbs). See LauncherToastListener.tsx + actions/unifideck_uri.py.",
		openStorageManager: "تفريغ مساحة",
		signInToStore: "تسجيل الدخول",
		retrySync: "إعادة المحاولة",
		retryLibrarySync: "تحديث المكتبة",
		openSaveFolder: "فتح مجلد الحفظ",
		showLogs: "إظهار السجلات"
	},
	library: {
		_comment: "Toasts emitted by SyncService when a per-store library sync fails. The retry button triggers a fire-and-forget refresh via the unifideck://refresh-library/<store> verb; the user doesn't wait for the result inline, the Library view picks it up through its SYNC_PROGRESS subscription.",
		syncStoreFailed: "فشل تحديث مكتبة {{store}} ({{error}}). ستبقى مكتبتك الحالية ظاهرة."
	},
	cloudPullStarted: "جاري تنزيل الحفظ السحابي...",
	cloudPushStarted: "جاري رفع الحفظ المحلي...",
	cloudSavePathFailed: "تعذر تحديد موقع الحفظ",
	cloudSavePathSet: "تم تحديث موقع الحفظ",
	cloudPullDone: "تم تنزيل الحفظ السحابي",
	cloudPullFailed: "فشل تنزيل الحفظ السحابي",
	cloudPushDone: "تم رفع الحفظ المحلي",
	cloudPushFailed: "فشل رفع الحفظ المحلي"
};
var uninstallModal = {
	title: "تأكيد إلغاء التثبيت",
	description: "هل أنت متأكد من رغبتك في إلغاء تثبيت {{title}}؟",
	deleteProtonLabel: "حذف ملفات Proton أيضاً",
	deleteProtonDescription: "يزيل بيانات التوافق وملفات المستخدم الخاصة باللعبة",
	warningTitle: "تحذير:",
	warningBody: "سيؤدي هذا إلى حذف بيانات الحفظ المخزنة داخل بيئة تشغيل Wine نهائياً. تأكد من وجود نسخة احتياطية لديك.",
	cancel: "إلغاء",
	uninstall: "إلغاء التثبيت"
};
var languageSettings = {
	title: "اللغة",
	label: "لغة العرض",
	description: "لغة الواجهة وتنزيل الألعاب (إن كانت مدعومة)",
	autoDetect: "تلقائي (لغة النظام)"
};
var errors = {
	legendaryNotFound: "أداة Epic Games CLI (Legendary) غير موجودة",
	gogdlNotFound: "أداة GOG CLI (gogdl) غير موجودة",
	nileNotFound: "أداة Amazon Games CLI (Nile) غير موجودة",
	authUrlFailed: "فشل الحصول على رابط المصادقة",
	authFailed: "فشلت المصادقة",
	notAuthenticated: "غير مصادق عليه. يرجى تسجيل الدخول أولاً.",
	syncInProgress: "المزامنة قيد التشغيل بالفعل",
	syncCancelled: "تم إلغاء المزامنة بواسطة المستخدم",
	gameNotFound: "اللعبة غير موجودة",
	gameNotInstalled: "اللعبة غير مثبتة",
	installFailed: "فشل التثبيت — راجع السجلات لمزيد من التفاصيل",
	installNoDirectory: "اكتمل التثبيت ولكن تعذر تحديد مجلد اللعبة",
	uninstallFailed: "فشل إلغاء التثبيت",
	lockConflict: "تعارض في القفل — يرجى المحاولة لاحقاً",
	unknownStore: "متجر غير معروف",
	shortcutWriteFailed: "فشلت كتابة الاختصارات",
	backgroundSyncDisabled: "المزامنة الخلفية معطلة",
	steamGridDbUnavailable: "مكتبة SteamGridDB غير متاحة",
	deleteSyncInProgress: "لا يمكن إجراء الحذف أثناء تشغيل المزامنة",
	alreadyInQueue: "موجود بالفعل في قائمة الانتظار",
	download: {
		diskSpace: "مساحة القرص غير كافية",
		insufficientSpace: "Not enough space: needs {{need}} GB, only {{free}} GB free.",
		network: "خطأ في الشبكة — يرجى التحقق من اتصالك بالإنترنت",
		authExpired: "انتهت صلاحية الجلسة — يرجى تسجيل الدخول مرة أخرى",
		toolMissing: "أداة التنزيل المطلوبة غير موجودة",
		processFailed: "فشل التنزيل — يرجى المحاولة مرة أخرى",
		directoryNotFound: "تعذر العثور على ملفات اللعبة المثبتة",
		lockConflict: "هناك عملية أخرى قيد التشغيل — يرجى المحاولة لاحقاً",
		ubisoftClient: "خطأ في عميل Ubisoft Connect — يرجى محاولة إصلاح بيئة التشغيل",
		ubisoftClientNotRunning: "عميل Ubisoft Connect لا يستجيب",
		ubisoftBootstrap: "تعذر تهيئة بيئة تشغيل Ubisoft Connect",
		ubisoftUmuMissing: "أداة umu-run غير موجودة — مطلوبة لألعاب Ubisoft",
		generic: "فشل التثبيت — راجع السجلات للتفاصيل"
	},
	unknown: "خطأ غير معروف",
	steamAppsUnavailable: "واجهة برمجة تطبيقات SteamClient.Apps غير متوفرة"
};
var sync = {
	start: "مزامنة المكتبات",
	force: "مزامنة إجبارية",
	syncing: "جاري المزامنة…",
	cancel: "إلغاء المزامنة",
	cancelling: "جاري الإلغاء…",
	preparing: "جاري التحضير…",
	downloadingArtwork: "جاري تنزيل الغلاف للعبة {{game}}...",
	fetchingGameLists: "جاري جلب قوائم الألعاب...",
	fetchingStore: "جاري جلب ألعاب {{store}} ({{current}}/{{total}})...",
	cancelledByUser: "تم إلغاء المزامنة بواسطة المستخدم",
	checkingInstalledGames: "جاري التحقق من الألعاب المثبتة...",
	sgdbLookup: "البحث عن {{count}} من الألعاب في SteamGridDB...",
	extractingSteamMetadata: "جاري استخراج البيانات الوصفية لـ Steam...",
	fetchingEnhancedMetadata: "جاري جلب البيانات الوصفية المحسنة...",
	lookingUpUnifiDB: "جاري جلب البيانات الوصفية لـ unifiDB...",
	fetchingMetacriticData: "جاري جلب بيانات Metacritic...",
	fetchingCompatData: "جاري التحقق من التوافق...",
	checkingExistingArtwork: "جاري التحقق من الأغلفة الحالية...",
	cleaningOrphanedArtwork: "جاري تنظيف الأغلفة غير المستخدمة...",
	retryingMissingArtwork: "إعادة المحاولة لـ {{synced}} / {{total}} من الألعاب التي تفتقد للأغلفة...",
	savingShortcuts: "جاري حفظ الاختصارات...",
	completed: "اكتملت المزامنة!",
	extractingMetadata: "جاري استخراج البيانات الوصفية لـ {{game}}...",
	cooldown: "يرجى الانتظار {{secs}} ثانية",
	artworkPhase: "غلاف {{done}} / {{total}}"
};
var force_sync = {
	migratingOldInstallations: "مزامنة إجبارية: ترحيل عمليات التثبيت القديمة...",
	fetchingGameLists: "مزامنة إجبارية: جلب قوائم الألعاب...",
	checkingInstalledGames: "مزامنة إجبارية: التحقق من الألعاب المثبتة...",
	rewritingShortcuts: "مزامنة إجبارية: إعادة كتابة الاختصارات...",
	cancelledShortcutsSaved: "تم إلغاء المزامنة الإجبارية — تم حفظ الاختصارات",
	writingShortcuts: "مزامنة إجبارية: كتابة الاختصارات...",
	clearingProton: "مزامنة إجبارية: تنظيف بيئات توافق Proton...",
	completed: "اكتملت المزامنة الإجبارية! تم تحديث {{updated}} ألعاب، وتم تنزيل {{artwork}} من الأغلفة."
};
var artwork = {
	checking: "جاري التحقق من الأغلفة...",
	queueRefresh: "إضافة جميع الألعاب إلى قائمة الانتظار لتحديث الأغلفة...",
	downloadProgress: "الأغلفة المنزلة: {{synced}}/{{total}} ({{game}})"
};
var deckTabs = {
	greatOnDeck: "ممتازة على Deck",
	allGames: "جميع الألعاب",
	installed: "مثبتة",
	nonSteam: "ألعاب خارجية (غير Steam)",
	steam: "Steam",
	epic: "Epic",
	gog: "GOG",
	amazon: "Amazon",
	ubisoft: "Ubisoft",
	microsoft: "Microsoft"
};
var unifiedLibrary = {
	error: "خطأ في Unifideck",
	errorLoadingView: "فشل تحميل مكتبة الألعاب الموحدة",
	checkConsole: "يرجى مراجعة سجلات المتصفح لمزيد من التفاصيل",
	errorLoadingGames: "حدث خطأ أثناء تحميل قائمة الألعاب",
	tryReloading: "يرجى تجربة إعادة تشغيل الملحق أو مراجعة سجلات المتصفح",
	storeLabel: "المتجر:",
	allStores: "جميع المتاجر",
	searchPlaceholder: "البحث عن ألعاب...",
	library: "المكتبة"
};
var gameInfoPanel = {
	loading: "جاري تحميل معلومات اللعبة...",
	noMetadata: "لا توجد بيانات وصفية متاحة لهذه اللعبة.",
	noTestResults: "لا توجد نتائج اختبار تفصيلية متاحة.",
	buttons: {
		details: "التفاصيل",
		synopsis: "نبذة",
		storePage: "صفحة المتجر",
		dlc: "الإضافات (DLC)",
		communityHub: "مركز المجتمع",
		pointsShop: "متجر النقاط",
		discussions: "المناقشات",
		guides: "البرامج التعليمية",
		support: "الدعم الفني",
		install: "تثبيت",
		uninstall: "إلغاء التثبيت",
		cancel: "إلغاء",
		achievements: "الإنجازات"
	},
	labels: {
		store: "المتجر:",
		name: "الاسم:",
		size: "الحجم:",
		developer: "المطور:",
		publisher: "الناشر:",
		released: "تاريخ الإصدار:",
		metacritic: "Metacritic:",
		status: "الحالة:",
		spaceRequired: "المساحة المطلوبة",
		lastPlayed: "آخر تشغيل",
		playTime: "وقت التشغيل",
		never: "أبداً"
	},
	compatibility: {
		modalTitle: "التوافق مع Steam Deck",
		viewOnProtonDb: "عرض على ProtonDB",
		close: "إغلاق",
		verified: "معتمد بالكامل",
		playable: "قابل للعب",
		unsupported: "غير مدعوم",
		unknown: "غير معروف"
	},
	failedToLoadMetadata: "فشل تحميل البيانات الوصفية",
	lastSession: "آخر جلسة: تم فتح {{names}}"
};
var gameDetailsSettings = {
	title: "واجهة تفاصيل اللعبة",
	simple: "مبسطة",
	detailed: "تفصيلية"
};
var gogLanguageModal = {
	title: "اختر اللغة",
	description: "اختر لغة التنزيل للعبة {{title}}",
	label: "اللغة",
	install: "تثبيت",
	cancel: "إلغاء"
};
var accountSwitch = {
	title: "تم رصد تغيير الحساب",
	description: "لقد قمت بتسجيل الدخول بحساب Steam مختلف. كيف ترغب في التعامل مع بيانات Unifideck؟",
	migrate: "نقل بيانات Unifideck",
	migrateDescription: "نسخ الاختصارات والأغلفة من الحساب السابق",
	freshStart: "بداية جديدة",
	freshStartDescription: "مسح تسجيلات دخول المتاجر المحفوظة لربط حسابات Epic/GOG/Amazon/Ubisoft أخرى",
	skip: "تخطي",
	toastMigrateTitle: "اكتمل نقل البيانات",
	toastMigrateBody: "تم نقل {{shortcuts}} من الاختصارات، و {{artwork}} من الأغلفة",
	toastClearTitle: "تم مسح بيانات الاعتماد",
	toastClearBody: "تم إعادة تعيين حالة تسجيل دخول المتجر. يمكنك الآن تسجيل الدخول بحساب آخر."
};
var ubisoftAuth = {
	title: "تسجيل الدخول إلى Ubisoft Connect",
	description: "سجل الدخول إلى حساب Ubisoft Connect لربط مكتبة ألعابك.",
	signInViaUpc: "تسجيل الدخول عبر Ubisoft Connect",
	upcDescription: "سيتم فتح Ubisoft Connect لتسجيل الدخول. بمجرد تسجيل الدخول بنجاح، سيتم استيراد بيانات جلسة العمل تلقائياً.",
	launchingUpc: "جاري تشغيل Ubisoft Connect...",
	waitingForLogin: "بانتظار تسجيل الدخول...",
	retryLaunch: "إعادة محاولة التشغيل",
	cancel: "إلغاء",
	launchFailed: "فشل تشغيل Ubisoft Connect. يرجى المحاولة مرة أخرى.",
	signingIn: "جاري تسجيل الدخول...",
	emailLabel: "البريد الإلكتروني",
	passwordLabel: "كلمة المرور",
	signIn: "تسجيل الدخول",
	twoFactorTitle: "المصادقة الثنائية",
	twoFactorDescription: "يرجى إدخال رمز التحقق المكون من قطعتين المعروض في تطبيق المصادقة على هاتفك.",
	twoFactorLabel: "رمز التحقق",
	verify: "تحقق",
	verifying: "جاري التحقق...",
	errorEmptyFields: "يرجى إدخال البريد الإلكتروني وكلمة المرور",
	errorEmpty2FA: "يرجى إدخال رمز المصادقة الثنائية",
	errorLoginFailed: "فشل تسجيل الدخول. يرجى التحقق من بيانات الاعتماد الخاصة بك.",
	errorInvalid2FA: "رمز التحقق غير صحيح. يرجى المحاولة مرة أخرى.",
	showPassword: "إظهار كلمة المرور",
	hidePassword: "إخفاء كلمة المرور",
	doubleLoginWarning: "قد تضطر إلى تسجيل الدخول إلى Ubisoft مرتين."
};
var relativeDate = {
	today: "اليوم",
	yesterday: "أمس"
};
var common = {
	thisGame: "هذه اللعبة",
	game: "لعبة",
	unknown: "غير معروف",
	ok: "موافق",
	cancel: "إلغاء",
	working: "جاري المعالجة…",
	loading: "جاري التحميل…",
	free: "مجاني",
	close: "إغلاق"
};
var info = {
	expandDetails: "عرض التفاصيل",
	collapseDetails: "إخفاء التفاصيل"
};
var microsoft = {
	notCompatible: "صيغة اللعبة هذه غير متوافقة مع SteamOS",
	playOnCloud: "اللعب عبر السحاب",
	xcloudStreaming: "Xbox Cloud Gaming",
	signInMessage: "تسجيل الدخول إلى حساب Microsoft / Xbox الخاص بك",
	accountConnected: "تم ربط حساب Microsoft",
	loggedOut: "تم تسجيل الخروج من Microsoft Store",
	xcloudNotInstalled: "لا تتطلب ألعاب xCloud التثبيت محلياً",
	chromiumRequired: "مطلوب Microsoft Edge",
	chromiumRequiredMessage: "تتطلب عملية تسجيل الدخول متصفح Microsoft Edge. هل ترغب في تنزيله وتثبيته تلقائياً؟ (يتم هذا الإجراء لمرة واحدة فقط).",
	chromiumInstallButton: "تثبيت Microsoft Edge",
	chromiumInstalling: "جاري تثبيت Microsoft Edge...",
	chromiumInstalled: "تم تثبيت Microsoft Edge بنجاح!",
	chromiumAlreadyInstalled: "متصفح متوافق مثبت بالفعل",
	chromiumInstallFailed: "فشل تثبيت Microsoft Edge. يرجى محاولة التثبيت يدوياً: flatpak install com.microsoft.Edge",
	chromiumInstallTimeout: "انتهت مهلة تثبيت Microsoft Edge. يرجى التحقق من اتصال الشبكة.",
	browserAlreadyInstalled: "متصفح متوافق مثبت بالفعل",
	browserInstalled: "تم تثبيت Microsoft Edge بنجاح!",
	browserInstallFailed: "فشل تثبيت Microsoft Edge. يرجى محاولة التثبيت يدوياً: flatpak install com.microsoft.Edge",
	flatpakNotFound: "بيئة Flatpak غير موجودة في النظام",
	signInRequired: "تسجيل الدخول مطلوب للعب",
	syncSkippedNoSubscription: "لم يتم رصد اشتراك Xbox Game Pass نشط. تم تخطي مزامنة ألعاب xCloud.",
	syncSkippedTierUnknown: "تم رصد اشتراك Xbox Game Pass ولكن تعذر تأكيد الفئة. لتفادي إظهار الألعاب غير القابلة للتشغيل، تم تخطي المزامنة.",
	subscriptionCheckFailed: "تعذر التحقق من حالة اشتراك Xbox Game Pass. تم تخطي مزامنة xCloud. يرجى المحاولة عند توفر اتصال شبكة مستقر.",
	subscriptionDetected: "تم رصد اشتراك Xbox Game Pass نشط من الفئة {{tier}}."
};
var playButton = {
	play: "تشغيل",
	install: "تثبيت",
	cancel: "إلغاء",
	retry: "إعادة المحاولة",
	downloadFailed: "فشل التنزيل",
	installing: "جاري التثبيت",
	controllerConfig: "إعدادات أداة التحكم",
	appSettings: "إعدادات التطبيق"
};
var play = {
	play: "ابدأ اللعب",
	resume: "استئناف",
	stop: "إيقاف",
	cancel: "إلغاء",
	cancelling: "جاري الإلغاء…",
	update: "تحديث",
	uninstall: "إلغاء التثبيت",
	queued: "في الانتظار",
	downloading: "جاري التنزيل",
	extracting: "جاري الاستخراج",
	verifying: "جاري التحقق",
	finalizing: "جاري إنهاء التثبيت",
	cancelConfirmTitle: "إلغاء التنزيل؟",
	cancelConfirmBody: "هل أنت متأكد من إلغاء تثبيت {{title}}؟ سيتم حذف ملفات التنزيل غير المكتملة.",
	cancelConfirmConfirm: "إلغاء التنزيل",
	cancelConfirmCancel: "مواصلة التنزيل",
	playOnCloud: "اللعب عبر السحاب",
	cloudSave: {
		autoPullOn: "يتم تنزيل الحفظ السحابي تلقائيًا عند التشغيل.",
		close: "إغلاق",
		download: "تنزيل الحفظ السحابي",
		filesCount: "{{count}} ملفات",
		label: "الحفظ السحابي",
		menuTitle: "الحفظ السحابي — {{game}}",
		noCloudData: "غير معروف",
		noCloudSupport: "لا يوجد دعم للحفظ السحابي",
		setLocation: "تحديد موقع الحفظ",
		setLocationHint: "اختر المجلد الذي تخزن فيه هذه اللعبة ملفات حفظها.",
		setLocationTitle: "اختر مجلد الحفظ الخاص بهذه اللعبة",
		statusCloudAvailable: "الحفظ السحابي متاح",
		statusNoSaves: "لا توجد ملفات حفظ محلية",
		statusSyncing: "جاري مزامنة الحفظ السحابي...",
		statusUnresolved: "لم يتم العถور على موقع الحفظ — قد لا تعمل المزامنة لهذه اللعبة.",
		unresolvedHint: "قم بتشغيل اللعبة مرة واحدة حتى نتمكن من اكتشاف مجلد الحفظ الخاص بها، أو حدد الموقع يدويًا.",
		upload: "رفع الحفظ المحلي"
	},
	exe: {
		browse: "تصفح…",
		changeFailed: "تعذر تغيير الملف التنفيذي",
		changed: "تم تغيير الملف التنفيذي",
		defaultTag: "(الافتراضي)",
		menuItem: "تغيير الملف التنفيذي…",
		reset: "إعادة تعيين إلى الملف التنفيذي الافتراضي",
		resetButton: "إعادة تعيين للافتراضي",
		subtitle: "اختر الملف الذي يقوم بتشغيل اللعبة (على سبيل المثال: تخطي المشغل وتشغيل اللعبة مباشرة). مجلد العمل والحفظ السحابي لن يتأثرا.",
		title: "تغيير الملف التنفيذي — {{game}}",
		unavailable: "تعذر قراءة مجلد هذه اللعبة."
	}
};
var playMeta = {
	spaceRequired: "المساحة المطلوبة",
	installedSize: "المساحة المستخدمة",
	lastPlayed: "آخر تشغيل",
	neverPlayed: "لم يتم اللعب بها قط",
	hoursShort: "ساعة",
	minutesShort: "دقيقة",
	played: "وقت اللعب"
};
var downloads = {
	title: "التنزيلات",
	empty: "لا توجد تنزيلات جارية. قم بتثبيت الألعاب من مكتبتك لمتابعتها هنا.",
	current: "جاري التنزيل",
	queued: "قائمة الانتظار",
	finished: "اكتمل مؤخراً",
	cancel: "إلغاء",
	removeFromQueue: "إزالة من قائمة الانتظار",
	play: "لعب",
	outcome: {
		queued: "في الانتظار",
		downloading: "جاري التنزيل",
		completed: "مكتمل",
		cancelled: "ملغى",
		error: "خطأ"
	}
};
var navigation = {
	loading: "جاري التحميل...",
	storesTitle: "ربط المتاجر",
	syncTitle: "مزامنة المكتبة",
	languageTitle: "إعدادات اللغة"
};
var security = {
	panelTitle: "الأمان والحماية",
	counters: {
		label: "العدادات",
		empty: "لم يتم رصد أي أحداث أمنية منذ بدء التشغيل"
	},
	log: {
		label: "سجل التدقيق",
		entryCount: "سجل واحد",
		entryCount_other: "{{count}} سجلات",
		emptyFiltered: "لا توجد أحداث في السجل تطابق الفلاتر الحالية"
	},
	filter: {
		label: "فلترة",
		all: "كل الأحداث"
	},
	timeline: {
		label: "مخطط الأحداث الأمنية خلال الـ 24 ساعة الماضية (حدث واحد)",
		label_other: "مخطط الأحداث الأمنية خلال الـ 24 ساعة الماضية ({{count}} أحداث)",
		axisStart: "-24 ساعة",
		axisEnd: "الآن"
	},
	relativeTime: {
		seconds: "منذ {{count}} ثوانٍ",
		minutes: "منذ {{count}} دقائق",
		hours: "منذ {{count}} ساعات",
		days: "منذ {{count}} أيام"
	},
	bruteforce: {
		warningTitle: "تحذير: تم رصد محاولة اختراق بالقوة العمياء (مستوى تحذيري)",
		escalationTitle: "تنبيه: تم رصد محاولة اختراق بالقوة العمياء (مستوى خطر)",
		detail: "حدثت {{failures}} محاولات فك تشفير فاشلة خلال الـ {{window}} ثانية الماضية. الحدود المعينة: {{warning}} (تحذير) / {{escalation}} (تنبيه).",
		resetButton: "تأكيد وإعادة تعيين الكاشف"
	},
	actions: {
		clearLog: "مسح السجل والعدادات"
	},
	events: {
		tokenEncrypted: "تم تشفير الرمز المميز",
		tokenDecrypted: "تم فك تشفير الرمز المميز",
		decryptFailed: "فشل فك التشفير",
		tokenFileMigrated: "تم ترحيل ملف الرموز المميزة",
		legacyPlaintextDetected: "تم رصد بيانات غير مشفرة قديمة",
		authFlowStarted: "بدأت عملية المصادقة",
		authFlowCompleted: "اكتملت عملية المصادقة",
		authFlowFailed: "فشلت عملية المصادقة",
		permissionsCheck: "اكتمل التحقق من الصلاحيات",
		permissionsRepaired: "تم إصلاح الصلاحيات بنجاح",
		bruteforceSuspected: "يُشتبه في هجوم بالقوة العمياء",
		deviceResetDetected: "تم رصد إعادة تعيين الجهاز",
		fingerprintInitialized: "تمت تهيئة بصمة الجهاز",
		externalAuthCheckFailed: "فشل التحقق من الاتصال الخارجي للمصادقة",
		configValidationCompleted: "اكتمل التحقق من صحة التكوين",
		configValidationFailed: "فشل التحقق من صحة التكوين"
	},
	configValidation: {
		degradedTitle: "خطأ في التكوين: تشغيل بوضع محدود",
		detail: "تم رصد خطأ واحد في التكوين أثناء بدء التشغيل",
		detail_other: "تم رصد {{count}} أخطاء في التكوين أثناء بدء التشغيل",
		sourceDefaults: "الخيارات الافتراضية",
		sourceUser: "إعدادات المستخدم",
		sourceMerged: "المدمجة",
		overflow: "+ خطأ آخر",
		overflow_other: "+ {{count}} أخطاء أخرى"
	}
};
var cloudSyncBehavior = {
	_comment: "لوحة الإعدادات للمستخدم لتهيئة سلوك إشعارات أخطاء مزامنة الحفظ السحابي لكل متجر على حدة. ترتبط store_label بصفوف جدول CloudSyncBehaviorPanel.tsx. تم استبعاد Microsoft لعدم وجود حفظ سحابي محلي لـ xCloud.",
	title: "سلوك المزامنة السحابية",
	description: "حدد كيف يتصرف Unifideck عند حدوث مشكلة أثناء تنزيل الحفظ السحابي قبل اللعب أو رفعه بعد الخروج.",
	loadError: "تعذر تحميل الإعدادات الحالية",
	saveError: "فشل حفظ الإعدادات، يرجى المحاولة مرة أخرى",
	store_label: {
		"default": "الإعداد الافتراضي (جميع المتاجر)",
		epic: "Epic Games",
		gog: "GOG",
		amazon: "Amazon Games",
		ubisoft: "Ubisoft Connect"
	},
	mode: {
		silent: "صامت (تسجيل في ملف السجل فقط)",
		toast: "إشعار منبثق"
	}
};
var cloudSync = {
	_comment: "رسائل الخطأ المعروضة للمستخدم عند فشل المزامنة. يحدد مفتاح الخطأ error_i18n_key القادم من الواجهة الخلفية السلوك.",
	error: {
		network_unreachable: "الشبكة غير متوفرة",
		auth_expired: "انتهت صلاحية المصادقة، يرجى إعادة تسجيل الدخول",
		forbidden: "الطلب مرفوض (غير مسموح به)",
		quota_exceeded: "تجاوزت السعة التخزينية للسحابة",
		server_error: "خطأ في خوادم المتجر",
		timed_out: "انتهت مهلة الطلب",
		disk_full: "المساحة التخزينية ممتلئة",
		disk_space_low: "تم التخطي: المساحة المتبقية منخفضة جداً (حرر بعض المساحة)",
		permission_denied: "تم رفض الصلاحية",
		cancelled: "تم إلغاء العملية",
		unknown: "حدث خطأ غير متوقع في النظام"
	},
	unavailableNativeDep: "الحفظ السحابي معطل على هذا النظام — إصدار Python ({{python}}) يفتقد إلى مكون مطلوب. حفظك المحلي لم يتأثر؛ أعد بناء/تثبيت الملحق بعد تحديث النظام لاستعادة المزامنة."
};
var launchLogs = {
	_comment: "النصوص المستخدمة في نافذة LaunchLogsModal. تفتح عبر بروتوكول unifideck://show-logs/<launch_id>. تقرأ ملف السجل ~/.local/share/unifideck/launches/<launch_id>.log وتعرض الأسطر ملونة حسب خطورتها.",
	title: "سجل التشغيل — {{launchId}}",
	close: "إغلاق",
	copyAll: "نسخ الكل",
	"export": "تصدير إلى ملف",
	copied: "تم نسخ السجل إلى الحافظة بنجاح",
	copyFailed: "فشل نسخ السجل",
	exporting: "جاري التصدير…",
	exportedTo: "تم تصدير السجل بنجاح إلى {{path}}",
	exportFailed: "فشل التصدير: {{error}}",
	loading: "جاري تحميل سجل التشغيل…",
	notAvailable: "لم تعد سجلات هذا التشغيل متوفرة (تُحذف تلقائياً بعد مرور 7 أيام).",
	error: "تعذر قراءة ملف السجل: {{error}}",
	fileInfo: "{{path}} — معروض حالياً {{shown}} أسطر من أصل {{total}}",
	filterLabel: "تصفية حسب المستوى",
	filterInfo: "معلومات",
	filterWarning: "تحذير",
	filterError: "أخطاء",
	nextError: "الذهاب للخطأ التالي ({{current}}/{{total}})",
	noErrors: "لم يتم العثور على أخطاء في السجل",
	allFiltered: "تم إخفاء جميع الأسطر بناءً على الفلاتر المحددة. يرجى تحديد مستوى واحد على الأقل للعرض."
};
var settings = {
	_comment: "إعدادات UnifideckSettingsPanel الأساسية. refreshAll تطلق عملية مزامنة لجميع المتاجر النشطة في الخلفية.",
	storeConnections: "ربط حسابات المتاجر",
	librarySync: "مزامنة الألعاب",
	language: "اللغة",
	languageAuto: "تلقائي (حسب النظام)",
	refreshAll: {
		sectionTitle: "إجراءات مكتبة الألعاب",
		button: "مزامنة جميع المتاجر المتصلة",
		buttonBusy: "جاري بدء المزامنة الخلفية…",
		startedTitle: "تم إرسال أمر المزامنة",
		startedBody: "مزامنة جميع المتاجر المتصلة تجري الآن في الخلفية.",
		startedSingle: "مزامنة متجر {{store}} تجري الآن في الخلفية.",
		failedTitle: "فشل إرسال أمر المزامنة"
	}
};
var saveFolder = {
	_comment: "نافذة SaveFolderModal لتصفح ملفات الحفظ المحلية للمستخدم عند حدوث مشاكل في المزامنة السحابية. تفتح عبر unifideck://open-save-folder/<store>/<game_id>.",
	title: "ملفات الحفظ المحلية — {{store}} / {{gameId}}",
	close: "إغلاق",
	copyPath: "نسخ مسار المجلد",
	copyFileList: "نسخ قائمة الملفات",
	depthLabel: "عمق المجلدات",
	filterLabel: "البحث بالاسم",
	loading: "جاري قراءة هيكل المجلدات المحلية…",
	noSavesYet: "لم يتم العثور على ملفات حفظ محلية. سيتم إنشاء المجلد بعد أول مزامنة سحابية ناجحة للعبة.",
	aggregate: "يحتوي المجلد على {{count}} ملفات بحجم إجمالي {{size}}",
	truncated: "هناك {{count}} ملفات أخرى بحجم {{size}} غير معروضة في الجدول أدناه، يرجى تضييق نطاق البحث.",
	error: "حدث خطأ أثناء قراءة ملفات الحفظ المحلية: {{error}}"
};
var library = {
	_comment: "خصائص التحكم في الألعاب ضمن المكتبة، وتتضمن سلوك قاطع الدائرة في حال تكرار فشل التشغيل.",
	circuitBreaker: {
		_comment: "سلوك قاطع الدائرة. يُفعل تلقائياً عند تجاوز عدد مرات الفشل المعين (الافتراضي 3 مرات خلال 600 ثانية).",
		badgeRecent: "فشل التشغيل {{count}} مرات مؤخراً",
		badgeRecent_other: "فشل التشغيل {{count}} مرات مؤخراً",
		badgeTooltip: "تم تعليق التشغيل التلقائي لهذه اللعبة مؤقتاً بسبب تكرار فشل تشغيلها لـ {{count}} مرات متتالية.",
		forceLaunchButton: "تشغيل إجباري (تخطي التعليق)",
		resetButton: "إعادة تعيين عداد الأخطاء",
		failureKind: {
			fastBoot: "خروج مفاجئ بعد التشغيل",
			launcherError: "خطأ في تشغيل برنامج المتجر"
		}
	}
};
var pickStorage = {
	title: "اختر مسار التثبيت",
	customOption: "تحديد مسار مخصص...",
	customLabel: "مسار مخصص",
	change: "تغيير الإعدادات...",
	gameSize: "المساحة التقديرية للعبة"
};
var restart = {
	title: "اكتملت المزامنة!",
	body: "يرجى إعادة تشغيل Steam لتطبيق الألعاب والأغلفة الجديدة",
	confirm: "إعادة التشغيل الآن",
	later: "لاحقاً"
};
var updater = {
	checkButton: "التحقق من التحديثات",
	checkCompleteMessage: "تم جلب معلومات النسخة الأخيرة بنجاح.",
	checkCompleteTitle: "اكتمل التحقق من التحديثات",
	checkFailedTitle: "فشل التحقق",
	checkingButton: "جاري التحقق...",
	currentTitle: "الحالية",
	downgradeButton: "تراجع إلى النسخة v{{version}}",
	installFailedTitle: "فشل التثبيت",
	installTimeoutMessage: "لا استجابة من Decky Loader — قد يكون التثبيت قد توقف أو فشل. يُرجى المحاولة مرة أخرى.",
	installedLabel: "مثبت",
	installingButton: "جاري التثبيت...",
	installingTitle: "تثبيت الملحق",
	latestLabel: "الأحدث",
	modalTitle: "UNIFIDECK v{{version}} — سجل التغييرات",
	noBackend: "خدمة Decky الخلفية غير متوفرة.",
	noReleaseNotes: "لا تتوفر ملاحظات الإصدار.",
	reinstallButton: "إعادة تثبيت النسخة v{{version}}",
	refreshFailedMessage: "تعذّر التحقق من معلومات أحدث إصدار. تحقّق من اتصالك وحاول مرة أخرى.",
	releaseGoneMessage: "لم يعد هذا الإصدار متوفرًا. يُرجى التحقق من التحديثات والاختيار مرة أخرى.",
	releaseNotesButton: "ملاحظات الإصدار",
	stageDownload: "جاري التنزيل…",
	stageFinish: "جاري الإنهاء…",
	stageInstall: "جاري التثبيت…",
	stageOpen: "قراءة الحزمة…",
	stageParse: "جاري التحقق…",
	stageRemove: "إزالة النسخة القديمة…",
	stageStart: "جاري البدء…",
	stageWorking: "جاري التثبيت…",
	titleLoading: "جاري التحقق من النسخة",
	typeDowngrade: "تراجع إلى النسخة",
	typeReinstall: "إعادة تثبيت النسخة",
	typeUpdate: "تحديث إلى النسخة",
	updateButton: "تحديث إلى v{{version}}"
};
var achievements = {
	unlockedTitle: "تم فتح الإنجاز",
	unlockedBody: "{{name}}"
};
var achievementsModal = {
	title: "الإنجازات",
	progress: "{{unlocked}} / {{total}} ({{percent}}%)",
	refresh: "تحديث",
	close: "إغلاق",
	loading: "جارٍ تحميل الإنجازات…",
	empty: "لا تحتوي هذه اللعبة على إنجازات.",
	hidden: "إنجاز مخفي",
	unlockedOn: "تم الفتح في {{date}}",
	error: "تعذّر تحميل الإنجازات.",
	errorOffline: "اتصل بالإنترنت لعرض الإنجازات.",
	errorAuth: "سجّل الدخول إلى متجر هذه اللعبة لعرض الإنجازات.",
	errorUnavailable: "الإنجازات غير متوفرة لهذه اللعبة."
};
var collectionSettings = {
	title: "مجموعات STEAM",
	enabled: "تم تمكين المجموعات",
	disabled: "تم تعطيل المجموعات"
};
var arSA = {
	_comment: _comment,
	tabs: tabs,
	auth: auth,
	storage: storage,
	storageSettings: storageSettings,
	gameGrid: gameGrid,
	downloadsTab: downloadsTab,
	cloudSave: cloudSave,
	storeConnections: storeConnections,
	librarySync: librarySync,
	cleanup: cleanup,
	captureLogs: captureLogs,
	installButton: installButton,
	confirmModals: confirmModals,
	toasts: toasts,
	uninstallModal: uninstallModal,
	languageSettings: languageSettings,
	errors: errors,
	sync: sync,
	force_sync: force_sync,
	artwork: artwork,
	deckTabs: deckTabs,
	unifiedLibrary: unifiedLibrary,
	gameInfoPanel: gameInfoPanel,
	gameDetailsSettings: gameDetailsSettings,
	gogLanguageModal: gogLanguageModal,
	accountSwitch: accountSwitch,
	ubisoftAuth: ubisoftAuth,
	relativeDate: relativeDate,
	common: common,
	info: info,
	microsoft: microsoft,
	playButton: playButton,
	play: play,
	playMeta: playMeta,
	downloads: downloads,
	navigation: navigation,
	security: security,
	cloudSyncBehavior: cloudSyncBehavior,
	cloudSync: cloudSync,
	launchLogs: launchLogs,
	settings: settings,
	saveFolder: saveFolder,
	library: library,
	pickStorage: pickStorage,
	restart: restart,
	updater: updater,
	achievements: achievements,
	achievementsModal: achievementsModal,
	collectionSettings: collectionSettings
};

/**
 * src/i18n/locales.generated.ts — AUTO-GENERATED, DO NOT EDIT.
 *
 * This file is regenerated by scripts/gen_locale_imports.py
 * from defaults/config.json. Any manual edit will be
 * overwritten at the next build.
 *
 * To add or remove a language, edit the i18n.locales
 * array in defaults/config.json and rerun the generator
 * (happens automatically in the CI workflow).
 *
 * Reference: Technical Document v1.0 — Section 10 (i18n).
 */
/**
 * Canonical list of supported language tags, in the order
 * they should appear in the LanguageSelector dropdown.
 * Sourced from defaults/config.json — single source of truth.
 */
const SUPPORTED_LANGUAGES = [
    "en-US",
    "fr-FR",
    "de-DE",
    "es-ES",
    "it-IT",
    "pt-BR",
    "nl-NL",
    "pl-PL",
    "ru-RU",
    "uk-UA",
    "tr-TR",
    "ja-JP",
    "ko-KR",
    "zh-CN",
    "zh-TW",
    "ar-SA",
];
/**
 * Native-script display names for each language, shown
 * in the LanguageSelector dropdown regardless of the
 * current UI language.
 */
const LANGUAGE_NAMES$1 = {
    "en-US": "English",
    "fr-FR": "Français",
    "de-DE": "Deutsch",
    "es-ES": "Español",
    "it-IT": "Italiano",
    "pt-BR": "Português (Brasil)",
    "nl-NL": "Nederlands",
    "pl-PL": "Polski",
    "ru-RU": "Русский",
    "uk-UA": "Українська",
    "tr-TR": "Türkçe",
    "ja-JP": "Japanese",
    "ko-KR": "Korean",
    "zh-CN": "Simplified Chinese",
    "zh-TW": "Traditional Chinese",
    "ar-SA": "Arabic",
};
/**
 * Languages written right-to-left. When one of these is
 * active, document.documentElement.dir is set to "rtl".
 */
const RTL_LANGUAGES = new Set([
    "ar-SA",
]);
/**
 * Map from language tag to its loaded JSON resource,
 * ready to pass to i18next's init() function.
 */
const LOCALE_RESOURCES = {
    "en-US": { translation: enUS },
    "fr-FR": { translation: frFR },
    "de-DE": { translation: deDE },
    "es-ES": { translation: esES },
    "it-IT": { translation: itIT },
    "pt-BR": { translation: ptBR },
    "nl-NL": { translation: nlNL },
    "pl-PL": { translation: plPL },
    "ru-RU": { translation: ruRU },
    "uk-UA": { translation: ukUA },
    "tr-TR": { translation: trTR },
    "ja-JP": { translation: jaJP },
    "ko-KR": { translation: koKR },
    "zh-CN": { translation: zhCN },
    "zh-TW": { translation: zhTW },
    "ar-SA": { translation: arSA },
};

/**
 * src/i18n/translations.ts — i18n loader + runtime API.
 *
 * This module is intentionally thin. The canonical list of
 * supported languages, their native names, their RTL flag, and
 * the static imports of every {tag}.json locale file all live
 * in `locales.generated.ts`, which is auto-generated from
 * `defaults/config.json` by `scripts/gen_locale_imports.py`.
 *
 * Adding a new language requires ONLY adding an entry to the
 * `i18n.locales` array in defaults/config.json. The CI workflow
 * then regenerates `locales.generated.ts` and calls DeepL to
 * produce the corresponding `{tag}.json` file. This file (the
 * runtime API) never needs to change.
 *
 * What lives here:
 * - `initI18n()`: one-time i18next setup with all bundled
 *   locales and RTL direction applied to the document root
 * - `changeLanguage()`: runtime language switch that persists
 *   the choice and updates the document direction
 * - `isRTL()`: convenience re-export from the generated catalog
 * - `getSupportedLanguages()`: returns the canonical tuple
 * - `resolveAutoLanguage()`: navigator → supported tag → en-US
 * - `detectInitialLanguage()`: localStorage pref → auto → tag
 *
 * Reference: Technical Document v1.0 — Section 10 (i18n),
 * Figure 85.
 */
/** localStorage key holding the user's language *preference*
 *  — either the "auto" sentinel or a concrete supported tag. */
const LANG_STORAGE_KEY = "unifideck.lang";
/** Sentinel preference meaning "follow the system / browser
 *  language". Stored and displayed verbatim ; resolved to a
 *  concrete tag by `resolveAutoLanguage()` before i18next sees
 *  it (i18next has no "auto" resource bundle). */
const AUTO_DETECT$1 = "auto";
/**
 * Return true if the given language tag should be rendered in
 * right-to-left mode. Centralized here so callers don't have to
 * know which languages are RTL.
 */
function isRTL(lang) {
    return RTL_LANGUAGES.has(lang);
}
/**
 * Apply the correct `dir` and `lang` attributes to the document
 * root. Idempotent — safe to call multiple times with the same
 * value. Called internally by initI18n() and changeLanguage();
 * most callers should never invoke it directly.
 *
 * When an RTL language becomes active, the entire React tree
 * flips naturally: flexbox row directions reverse, text-align
 * defaults to right, scrollbars move to the left, etc. Any
 * component that wants to opt out of flipping for a specific
 * element can override with an explicit `dir="ltr"` attribute.
 */
function applyDirection(lang) {
    if (typeof document === "undefined")
        return; // SSR safety
    document.documentElement.dir = isRTL(lang) ? "rtl" : "ltr";
    document.documentElement.lang = lang;
}
/**
 * Initialize i18next with all bundled locales. Called once at
 * plugin startup. Safe to call multiple times (idempotent).
 *
 * Also applies the correct `dir` attribute to the document root
 * based on the detected initial language, so RTL languages
 * render correctly from the first frame.
 */
async function initI18n() {
    if (instance.isInitialized)
        return;
    const initialLang = detectInitialLanguage();
    await instance.use(initReactI18next).init({
        resources: LOCALE_RESOURCES,
        lng: initialLang,
        fallbackLng: "en-US",
        interpolation: { escapeValue: false },
        returnEmptyString: false,
    });
    applyDirection(initialLang);
}
/**
 * Resolve the "auto-detect" preference to a concrete supported
 * tag from the browser's navigator.language, falling back to
 * en-US. Called both at boot and whenever the user (re-)selects
 * "Auto-detect", so i18next always receives a real locale.
 */
function resolveAutoLanguage() {
    const browserLang = (typeof navigator !== "undefined" && navigator.language) || "en-US";
    const match = SUPPORTED_LANGUAGES.find((l) => l === browserLang || l.startsWith(browserLang.split("-")[0]));
    return match || "en-US";
}
/**
 * Detect the initial *active* language for i18next at boot.
 *
 * Reads the stored preference (`LANG_STORAGE_KEY`): a concrete
 * supported tag is used as-is ; "auto" or an absent/unknown
 * value resolves through `resolveAutoLanguage()`. i18next never
 * sees the "auto" sentinel — only a real locale tag.
 */
function detectInitialLanguage() {
    try {
        const saved = localStorage.getItem(LANG_STORAGE_KEY);
        if (saved && SUPPORTED_LANGUAGES.includes(saved)) {
            return saved;
        }
    }
    catch {
        // localStorage unavailable
    }
    return resolveAutoLanguage();
}

/**
 * React tree traversal helpers.
 *
 * Wraps `@decky/ui`'s `findInReactTree` with a typed
 * signature and a depth limit. Steam's React tree is deep
 * (up to ~30 levels in some app detail views) so we cap the
 * search to avoid pathological cases when a matcher never
 * matches.
 */

/** Walk a React element tree depth-first and return the
 *  first node satisfying `matcher`, or null if none found
 *  within MAX_DEPTH levels. */
function findInReactTree(root, matcher) {
    if (root == null)
        return null;
    try {
        const result = DFL.findInReactTree(root, matcher);
        return result ?? null;
    }
    catch {
        return null;
    }
}

/**
 * Resolves Steam's dynamic CSS class names for App Details.
 *
 * Steam minifies its CSS class names per build (e.g.
 * `Detail_4f7a2`). The `@decky/ui` package exports the
 * current names as constants we can re-export, but the names
 * themselves rotate every Steam version. Centralising the
 * lookup here means a Steam update is a one-file fix.
 */

/** Snapshot the current class names. Cached per session
 *  because @decky/ui exports are static within a Steam build. */
let cached = null;
/**
 * Resolve the current build's class names by walking
 * the React component tree starting from the active
 * app-details root. The result is cached for the
 * lifetime of the page.
 *
 * @returns the resolved class-name set, or `null` if
 *   the app-details root could not be found (typically
 *   because Steam has not finished mounting it yet —
 *   callers should retry on the next animation frame).
 */
function getAppDetailsClasses() {
    if (cached)
        return cached;
    cached = {
        details: pickFirstClass(DFL.appDetailsClasses),
        actionButton: pickFirstClass(DFL.appActionButtonClasses),
        playSection: pickFirstClass(DFL.playSectionClasses),
        header: pickFirstClass(DFL.appDetailsHeaderClasses),
        sectionStyler: pickFirstClass(DFL.basicAppDetailsSectionStylerClasses),
    };
    return cached;
}
/** `@decky/ui` returns class objects keyed by semantic name
 *  (`{ root: "Detail_4f7a2", ... }`). We grab the first
 *  value as a representative class for class-name matching. */
function pickFirstClass(classObj) {
    for (const key in classObj) {
        if (Object.prototype.hasOwnProperty.call(classObj, key)) {
            return classObj[key] ?? "";
        }
    }
    return "";
}

/**
 * Router patch lifecycle.
 *
 * Wraps `@decky/api`'s `routerHook.addPatch` with a typed
 * handle. Plugins must keep a reference to the handle and
 * call `.remove()` in their unload path; otherwise patches
 * leak across plugin reloads and accumulate.
 */
/** Best-effort cleanup of stale duplicate patches for ``path``
 *  whose ``.toString()`` matches our new ``patch``. Decky's
 *  spam-loading during plugin install can leave dangling
 *  references behind; without this, the old (stale) patch wins
 *  and our latest code never runs. Pattern mirrors TabMaster's
 *  ``addPatch`` helper. */
function purgeDuplicatePatches(path, patch) {
    try {
        const loader = window.DeckyPluginLoader;
        const patches = loader?.routerHook?.routerState?._routePatches?.get(path);
        if (!patches)
            return;
        const newSrc = patch.toString();
        for (const existing of [...patches]) {
            if (existing.toString() === newSrc) {
                try {
                    routerHook.removePatch(path, existing);
                }
                catch (e) {
                    console.warn("[SteamBridge] purge duplicate patch failed:", e);
                }
            }
        }
    }
    catch (e) {
        console.warn("[SteamBridge] purgeDuplicatePatches inspect failed:", e);
    }
}
/** Patch a router route. The `patch` function receives the
 *  rendered React node for the route and must return a
 *  (possibly modified) node. */
function addRouterPatch(path, patch) {
    let removed = false;
    let token;
    const typedPatch = patch;
    purgeDuplicatePatches(path, typedPatch);
    try {
        // addPatch returns a RoutePatch token that must be passed
        // back to removePatch to undo — not an unpatch function.
        token = routerHook.addPatch(path, typedPatch);
    }
    catch (e) {
        console.error("[SteamBridge] addRouterPatch failed:", e);
        return { remove: () => { } };
    }
    return {
        remove: () => {
            if (removed || !token)
                return;
            removed = true;
            try {
                routerHook.removePatch(path, token);
            }
            catch (e) {
                console.warn("[SteamBridge] router unpatch failed:", e);
            }
        },
    };
}

/**
 * Shared shortcut launch primitives.
 *
 * Types and helpers consumed by every store auth flow that
 * goes through Steam's RunGame API. Lives inside
 * SteamBridge because all of them poke at Steam internals
 * (`window.appStore.m_mapApps`, `window.SteamClient.Apps`)
 * — the same globals the rest of SteamBridge isolates.
 *
 * Centralising these primitives lets the auth launcher
 * (utils/authShortcutLaunch.ts) and the Ubisoft-specific
 * launcher (utils/ubisoftShortcutLaunch.ts) share a single
 * source of truth for context shape, return shape, and the
 * helpers that read app-store entries.
 *
 * Anti-pattern explicitly avoided : duplicating the
 * `ShortcutLaunchContext` interface inside each launcher,
 * which led to silent drift in the legacy code (Microsoft
 * launcher exported its own subtly different result type).
 */
/** Get app store entry. */
function getAppStoreEntry(appId) {
    const appStore = window.appStore;
    return appStore?.m_mapApps?.get?.(appId);
}
/** Resolve the canonical RunGame id for a Steam shortcut.
 *  Falls back to a stringified appId if Steam hasn't filled
 *  in the gameid yet. */
function getShortcutRunGameId(appId) {
    const entry = getAppStoreEntry(appId);
    const gameId = entry?.gameid;
    if (typeof gameId === "string" && gameId.length > 0) {
        return gameId;
    }
    try {
        const val = (BigInt(appId) << 32n) | 0x02000000n;
        return val.toString();
    }
    catch {
        return String(appId);
    }
}
/** Read Steam's display_status for a shortcut, with a
 *  fallback to per-client data when local data is empty.
 *  Returns undefined if the shortcut is not in Steam's
 *  in-memory app store. Status values are Steam-internal
 *  (no public enum) — observed values include
 *  1 = launching, 4 = running. */
function getShortcutDisplayStatus(appId) {
    const entry = getAppStoreEntry(appId);
    if (!entry)
        return undefined;
    const local = entry.local_per_client_data?.display_status;
    if (typeof local === "number")
        return local;
    const perClient = entry.per_client_data?.[0]?.display_status;
    return typeof perClient === "number" ? perClient : undefined;
}
/** Best-effort check : true if Steam's app-store reports the
 *  shortcut as currently running or launching. Used by every
 *  shortcut launcher to skip a redundant RunGame() call when
 *  the user re-clicks Sign-In or Play. */
function isShortcutAppRunning(appId) {
    const status = getShortcutDisplayStatus(appId);
    return status === 1 || status === 4;
}

/**
 * SteamBridge — façade over Steam internals.
 *
 * Wraps every Steam API call the plugin needs : app overview
 * lookups, router patches, lifetime notifications, controller
 * config queries. Each method has a typed signature and a
 * defensive fallback so the plugin degrades gracefully if a
 * Steam update breaks an internal.
 *
 * Singleton instance; instantiated by the plugin entry and
 * passed via React context to consumers.
 */
/**
 * Single boundary between Unifideck and Steam's runtime
 * internals (window.SteamClient, window.appStore, the
 * Router, app-details React tree). Every other module
 * imports from here, never directly from Steam globals.
 *
 * The bridge :
 *  - centralises the spoofing/patching logic so a Steam
 *    update breaks one file, not the whole codebase ;
 *  - returns typed projections instead of raw any-shapes ;
 *  - logs and degrades gracefully when an internal symbol
 *    has been renamed or removed.
 */
class SteamBridge {
    /** True once at least one SteamClient method has been seen.
     *  Used by the plugin to delay UI mount until Steam is ready. */
    isReady() {
        return typeof window.SteamClient?.Apps?.GetAppOverview === "function";
    }
    /** Look up an `AppOverview` for any Steam appid (real game
     *  or non-Steam shortcut). Returns null if Steam can't find
     *  the appid; never throws. */
    getAppOverview(appId) {
        try {
            return window.SteamClient?.Apps?.GetAppOverview(appId) ?? null;
        }
        catch {
            return null;
        }
    }
    /** Subscribe to "app started / app stopped" notifications.
     *  Returns an Unregisterable; callers must call
     *  `.unregister()` in their cleanup path. */
    onAppLifetime(callback) {
        const apis = window.SteamClient?.GameSessions;
        if (!apis?.RegisterForAppLifetimeNotifications) {
            return { unregister: () => { } };
        }
        return apis.RegisterForAppLifetimeNotifications(callback);
    }
    /** Subscribe to "Play" / "Install" / "Update" button clicks
     *  via Steam's GameAction event stream. */
    onGameAction(callback) {
        const apis = window.SteamClient?.Apps;
        if (!apis?.RegisterForGameActionStart) {
            return { unregister: () => { } };
        }
        return apis.RegisterForGameActionStart(callback);
    }
    /** Cancel an in-flight game action (Play / Install / etc).
     *  Used to intercept Play and re-route to our launcher. */
    cancelGameAction(gameActionId) {
        window.SteamClient?.Apps?.CancelGameAction(gameActionId);
    }
    /** Get all owned + non-Steam apps. The two lists overlap
     *  for shortcuts (a non-Steam game registered in
     *  shortcuts.vdf appears in both). De-dup is the caller's
     *  responsibility. */
    getAllApps() {
        const apis = window.SteamClient?.Apps;
        if (!apis)
            return [];
        try {
            const owned = apis.GetOwnedApps?.() ?? [];
            const nonSteam = apis.GetNonSteamApps?.() ?? [];
            return [...owned, ...nonSteam];
        }
        catch {
            return [];
        }
    }
    /** Read user's Steam collections. Used by the unified
     *  library view to filter games by collection. */
    getCollections() {
        const map = window.collectionStore?.userCollections;
        return map ? Array.from(map.values()) : [];
    }
    /** Launch a non-Steam shortcut via Steam's RunGame API.
     *
     *  ``RunGame``'s first argument must be the 64-bit shortcut
     *  *gameID*, NOT the 32-bit appid — passing the raw appid is a
     *  silent no-op for non-Steam shortcuts (this is why clicking
     *  Play did nothing). We resolve it with ``getShortcutRunGameId``
     *  and pass empty launch options + ``(-1, 100)`` so Steam uses
     *  the shortcut's own stored ``LaunchOptions``. Mirrors the
     *  proven auth-flow / controllerConfig call shape. */
    runGame(appId) {
        window.SteamClient?.Apps?.RunGame(getShortcutRunGameId(appId), "", -1, 100);
    }
    /** Force-terminate a running app — used by "Stop game". */
    terminateApp(appId, force = false) {
        window.SteamClient?.Apps?.TerminateApp(appId, force);
    }
    /** Patch a router route to mount our React content at the
     *  given path. Returns a handle whose `.remove()` undoes
     *  the patch. */
    addRouterPatch(path, patch) {
        return addRouterPatch(path, patch);
    }
    /** Find a node deep in a React tree matching the predicate.
     *  Used to inject our PlayButton wrapper into Steam's
     *  AppDetails component. */
    findInReactTree(root, matcher) {
        return findInReactTree(root, matcher);
    }
    /** Resolve the dynamic CSS class names Steam uses for app
     *  details (they change every Steam build). */
    getAppDetailsClasses() {
        return getAppDetailsClasses();
    }
}

/**
 * Temporary Steam shortcut helpers.
 *
 * Creating a non-Steam shortcut through ``SteamClient.Apps.AddShortcut``
 * is the only reliable way to get an entry Steam will actually launch
 * mid-session : Steam populates its in-memory ``appStore.m_mapApps`` (and
 * the ``gameid`` that ``RunGame`` needs) from this API immediately,
 * whereas a shortcut written straight into ``shortcuts.vdf`` by the
 * backend is only loaded into that map at Steam startup. So a freshly
 * backend-written shortcut launched in the same session fails with
 * Steam's "Game configuration unavailable" modal.
 *
 * Every store auth flow (utils/authShortcutLaunch.ts) and the Ubisoft
 * first-session fallback (utils/ubisoftShortcutLaunch.ts) use these
 * helpers to spin up a throwaway shortcut, launch it, and remove it once
 * its session ends.
 */
const SHORTCUT_POLL_DELAY_MS = 250;
const SHORTCUT_POLL_TIMEOUT_MS = 5000;
/** Safety-net delay for removing a temporary shortcut when the "game
 *  stopped" lifetime notification is never observed (missed event or a
 *  Steam API change). Sized past the launcher's 600s auth ceiling so it
 *  can't fire during a normal sign-in; on the happy path the shortcut is
 *  removed the instant the launched "game" stops. */
const TEMP_SHORTCUT_SAFETY_CLEANUP_MS = 10 * 60 * 1000;
/** App store. */
function appStore$1() {
    return window.appStore;
}
/** Get shortcut game ID string. */
function getShortcutGameIdString(appId) {
    const app = appStore$1()?.m_mapApps?.get?.(appId);
    const gameId = app?.gameid;
    return typeof gameId === "string" && gameId.length > 0 ? gameId : null;
}
/** Poll Steam's in-memory app store until the freshly-created
 *  shortcut has a non-empty ``gameid`` (the signed CRC Steam uses
 *  for ``RunGame``). Resolves ``null`` on timeout. */
async function waitForShortcutGameId(appId, minimumDelayMs = 0) {
    const startedAt = Date.now();
    const timeoutMs = Math.max(SHORTCUT_POLL_TIMEOUT_MS, minimumDelayMs);
    return new Promise((resolve) => {
        const poll = () => {
            const elapsed = Date.now() - startedAt;
            if (elapsed >= minimumDelayMs) {
                const gameId = getShortcutGameIdString(appId);
                if (gameId) {
                    resolve(gameId);
                    return;
                }
            }
            if (elapsed >= timeoutMs) {
                resolve(null);
                return;
            }
            window.setTimeout(poll, SHORTCUT_POLL_DELAY_MS);
        };
        window.setTimeout(poll, SHORTCUT_POLL_DELAY_MS);
    });
}
/** Create a temporary shortcut via Steam's ``AddShortcut`` API.
 *  Returns the new appId once Steam has registered a ``gameid`` for
 *  it, or ``null`` if creation/registration failed (the caller should
 *  surface a "restart Steam" error). The shortcut is removed by
 *  {@link scheduleTemporaryShortcutCleanup} once its session ends. */
async function createTemporaryShortcut(params) {
    const { appName, launcherPath, launchOptions, logTag } = params;
    const steamApps = window.SteamClient?.Apps;
    if (!steamApps?.AddShortcut)
        return null;
    const startDir = launcherPath.substring(0, launcherPath.lastIndexOf("/"));
    const newAppId = await steamApps.AddShortcut(appName, launcherPath, startDir, launchOptions);
    if (typeof newAppId !== "number" || newAppId <= 0)
        return null;
    steamApps.SetShortcutLaunchOptions?.(newAppId, launchOptions);
    const gameId = await waitForShortcutGameId(newAppId, SHORTCUT_POLL_DELAY_MS);
    if (!gameId) {
        console.error(`${logTag} Temp shortcut ${newAppId} never received a gameid`);
        try {
            steamApps.RemoveShortcut?.(newAppId);
        }
        catch (error) {
            console.error(`${logTag} Failed to clean up temp shortcut ${newAppId}:`, error);
        }
        return null;
    }
    console.log(`${logTag} Temp shortcut ready: appId=${newAppId}, gameId=${gameId}`);
    return newAppId;
}
/** Schedule temporary shortcut cleanup. Removes the entry only once its
 *  launched "game" has actually **stopped** (sign-in completed, aborted,
 *  or the window closed), never on a blind timer.
 *
 *  Why: in Gaming Mode each launched game gets its own gamescope/XWayland
 *  session (``STEAM_MULTIPLE_XWAYLANDS=1``). The login/UPC window renders
 *  on that per-app display. Calling ``RemoveShortcut`` while the game is
 *  still running ends the session and tears down its XWayland, so the
 *  window vanishes back to the Steam UI mid-flow. Waiting for Steam's
 *  "stopped" notification keeps the session — and the window — alive for
 *  the whole login. */
function scheduleTemporaryShortcutCleanup(appId, logTag) {
    const steamApps = window.SteamClient?.Apps;
    const cleanup = [];
    let removed = false;
    let sawRunning = false;
    const remove = (reason) => {
        if (removed)
            return;
        removed = true;
        for (const fn of cleanup)
            fn();
        try {
            steamApps?.RemoveShortcut?.(appId);
            console.log(`${logTag} Removed temp shortcut ${appId} (${reason})`);
        }
        catch (error) {
            console.error(`${logTag} Failed to remove temp shortcut ${appId}:`, error);
        }
    };
    const sub = window.SteamClient?.GameSessions?.RegisterForAppLifetimeNotifications?.((n) => {
        if (n.unAppID !== appId)
            return;
        if (n.bRunning) {
            sawRunning = true;
        }
        else if (sawRunning) {
            // The launcher (and with it the window) has exited; the session
            // is gone, so removing the entry now is safe.
            remove("game stopped");
        }
    });
    if (sub)
        cleanup.push(() => sub.unregister());
    // Safety net: clean up even if the "stopped" notification never lands,
    // but only well past the launcher's 600s auth ceiling so it can't fire
    // during a normal sign-in.
    const safetyTimer = window.setTimeout(() => remove("safety timeout"), TEMP_SHORTCUT_SAFETY_CLEANUP_MS);
    cleanup.push(() => window.clearTimeout(safetyTimer));
}

/**
 * Typed error raised when an RPC call fails. Carries the
 * route name and the typed backend error code so callers
 * can branch on the cause (auth-expired, network-timeout,
 * disk-full…) without parsing free-form messages.
 */
class RpcError extends Error {
    constructor(code, message, route, cause) {
        super(message);
        this.name = "RpcError";
        this.code = code;
        this.route = route;
        this.cause = cause;
    }
}
/** Heuristic mapper from raw rejection → typed code. Backend
 *  exposes structured errors via `Result.error` strings; we
 *  match a small allowlist of well-known prefixes and fall
 *  through to UNKNOWN for the rest. */
function mapRpcError(raw, route) {
    const message = extractMessage(raw);
    const code = inferCode(message);
    return new RpcError(code, message, route, raw);
}
/** Extract message. */
function extractMessage(raw) {
    if (raw instanceof Error)
        return raw.message;
    if (typeof raw === "string")
        return raw;
    if (raw && typeof raw === "object" && "message" in raw) {
        return String(raw.message);
    }
    return "RPC call failed";
}
/** Infer code. */
function inferCode(message) {
    const m = message.toLowerCase();
    if (m.includes("timeout") || m.includes("timed out"))
        return "TIMEOUT";
    if (m.includes("auth") && m.includes("required"))
        return "AUTH_REQUIRED";
    if (m.includes("not authenticated"))
        return "AUTH_REQUIRED";
    if (m.includes("store unavailable"))
        return "STORE_UNAVAILABLE";
    if (m.includes("not ready"))
        return "BACKEND_NOT_READY";
    if (m.includes("bad arguments") || m.includes("invalid")) {
        return "BAD_ARGUMENTS";
    }
    return "UNKNOWN";
}

/**
 * Generic typed RPC hooks.
 *
 * `useRPCQuery` — fire-and-cache, auto-runs on mount, exposes
 *   {data, error, loading, refetch}. Use for read endpoints
 *   (get_store_infos, get_storage_locations, ...).
 *
 * `useRPCMutation` — manual trigger, exposes {mutate, error,
 *   loading}. Use for write endpoints (install_game, sync, ...).
 *
 * `useRPC` — low-level escape hatch returning a callable
 *   bound to a route. Most callers should prefer the two
 *   higher-level hooks above.
 *
 * All three honour the same error mapping: rejections are
 * wrapped in `RpcError` with structured fields so consumers
 * can switch on `.code` instead of regex-matching strings.
 */
/** Type guard for the backend envelope. */
function isRpcEnvelope(v) {
    return typeof v === "object" && v !== null && "success" in v && "data" in v;
}
/** Unwrap the backend ``{success, error, data}`` envelope.
 *
 *  ``useRPC`` does this for component callers automatically, but
 *  modules outside React (`AuthDispatcher`, `EventBusClient`,
 *  shortcut launchers) call ``call()`` directly and must unwrap
 *  manually. Exported here so the unwrap logic lives in one
 *  place — a change to the envelope shape is a one-file fix.
 *
 *  On a non-success envelope this throws `RpcError`, mirroring
 *  the hook behaviour. Pass `{ throwing: false }` to opt out
 *  (e.g. when the caller wants to inspect the failure shape
 *  itself).
 */
function unwrapRpcEnvelope(raw, options = {}) {
    const { route = "<unknown>", throwing = true } = options;
    if (isRpcEnvelope(raw)) {
        if (!raw.success && throwing) {
            throw new RpcError("UNKNOWN", raw.error ?? "RPC call failed", route, raw);
        }
        return raw.data;
    }
    return raw;
}
/** Low-level RPC caller. Returns a stable async function
 *  bound to the given route. Automatically unwraps the
 *  backend `{success, error, data}` envelope so callers
 *  receive only the `data` payload. */
function useRPC(route) {
    return SP_REACT.useCallback(async (...args) => {
        try {
            // @decky/api's call signature is variadic-typed; we
            // erase the variadic at the boundary and re-impose
            // the strict type via the generic.
            const raw = await call(route, ...args);
            // Unwrap the backend envelope if present.
            if (isRpcEnvelope(raw)) {
                if (!raw.success) {
                    throw new RpcError("UNKNOWN", raw.error ?? "RPC call failed", route, raw);
                }
                return raw.data;
            }
            // Fallback: if the response is not enveloped (should
            // not happen with the current backend, but defensive).
            return raw;
        }
        catch (err) {
            if (err instanceof RpcError)
                throw err;
            throw mapRpcError(err, route);
        }
    }, [route]);
}
/** Auto-fetch on mount + expose refetch. Caller can also
 *  pass `enabled=false` to defer until ready. */
function useRPCQuery(route, args, options = {}) {
    const enabled = options.enabled !== false;
    const [data, setData] = SP_REACT.useState(null);
    const [error, setError] = SP_REACT.useState(null);
    const [loading, setLoading] = SP_REACT.useState(enabled);
    const cancelledRef = SP_REACT.useRef(false);
    const rpc = useRPC(route);
    // Args identity changes per render; stringify for stable dep.
    const argsKey = JSON.stringify(args);
    /** Refetch. */
    const refetch = SP_REACT.useCallback(async () => {
        if (cancelledRef.current)
            return;
        setLoading(true);
        setError(null);
        try {
            const result = await rpc(...args);
            if (!cancelledRef.current)
                setData(result);
        }
        catch (e) {
            if (!cancelledRef.current)
                setError(e);
        }
        finally {
            if (!cancelledRef.current)
                setLoading(false);
        }
    }, [rpc, argsKey]); // eslint-disable-line react-hooks/exhaustive-deps
    SP_REACT.useEffect(() => {
        cancelledRef.current = false;
        if (enabled)
            void refetch();
        return () => {
            cancelledRef.current = true;
        };
    }, [enabled, refetch]);
    return { data, error, loading, refetch };
}
/** Manual-trigger RPC. The returned `mutate` resolves with
 *  the return value or null on error (error stored on state).
 *  Throwing variant available via `mutateOrThrow`. */
function useRPCMutation(route) {
    const rpc = useRPC(route);
    const [error, setError] = SP_REACT.useState(null);
    const [loading, setLoading] = SP_REACT.useState(false);
    const mutate = SP_REACT.useCallback(async (...args) => {
        setLoading(true);
        setError(null);
        try {
            return await rpc(...args);
        }
        catch (e) {
            setError(e);
            return null;
        }
        finally {
            setLoading(false);
        }
    }, [rpc]);
    /** Reset. */
    const reset = SP_REACT.useCallback(() => {
        setError(null);
        setLoading(false);
    }, []);
    return { mutate, error, loading, reset };
}

/**
 * RPC route registry — single source of truth.
 *
 * Every route in this table is documented in the backend
 * operational plan PDF and registered in one of the RPC
 * mixins (Store, Sync, Download, Launch, Playtime, UI,
 * Action, CloudFailure, Observability, Account, Security,
 * ConfigValidation).
 *
 * Components import these constants so a backend rename is a
 * one-file change on the TS side ; raw string method names
 * never appear elsewhere.
 */
const rpcRoutes = {
    // Store + auth (StoreRPCMixin)
    storeAuth: "store_auth",
    checkStoreStatus: "check_store_status",
    getStoreInfos: "get_store_infos",
    clearStoreAuths: "clear_store_auths",
    installEdge: "install_edge",
    // Auth-shortcut context (AuthShortcutsRPCMixin)
    getEpicAuthShortcutContext: "get_epic_auth_shortcut_context",
    getGogAuthShortcutContext: "get_gog_auth_shortcut_context",
    getAmazonAuthShortcutContext: "get_amazon_auth_shortcut_context",
    getMicrosoftAuthShortcutContext: "get_microsoft_auth_shortcut_context",
    getUbisoftAuthShortcutContext: "get_ubisoft_auth_shortcut_context",
    getCompatToolForGame: "get_compat_tool_for_game",
    // Library sync (SyncRPCMixin)
    syncLibraries: "sync_libraries",
    forceSyncLibraries: "force_sync_libraries",
    cancelSync: "cancel_sync",
    requestAuthSync: "request_auth_sync",
    getSyncProgress: "get_sync_progress",
    getGameSizeBytes: "get_game_size_bytes",
    updateSteamOwnedTitles: "update_steam_owned_titles",
    setActiveSteamUser: "set_active_steam_user",
    // Orphaned-shortcut sweep — boot-time cleanup (CleanupRPCMixin)
    scanOrphanedShortcuts: "scan_orphaned_shortcuts",
    // Downloads (DownloadRPCMixin)
    installGame: "install_game",
    uninstallGame: "uninstall_game",
    cancelDownload: "cancel_download",
    getDownloadQueue: "get_download_queue",
    // Game info / metadata (StoreRPCMixin)
    getGameInfo: "get_game_info",
    getGameMetadataDisplay: "get_game_metadata_display",
    getStorageLocations: "get_storage_locations",
    getGogGameLanguages: "get_gog_game_languages",
    getProtondbCache: "get_protondb_cache",
    // Library facets — per-shortcut enrichment for native Sort/Filters
    // + shortcut-keyed Great-on-Deck compat (LibraryFacetsRPCMixin)
    getOverviewEnrichment: "get_overview_enrichment",
    // Steam Store spoofing (StoreRPCMixin)
    getRealSteamAppidMappings: "get_real_steam_appid_mappings",
    getSteamMetadataCache: "get_steam_metadata_cache",
    injectGameToAppinfo: "inject_game_to_appinfo",
    // Library cleanup (SyncRPCMixin)
    performFullCleanup: "perform_full_cleanup",
    setLanguagePreference: "set_language_preference",
    getLanguagePreference: "get_language_preference",
    setDefaultStorageLocation: "set_default_storage_location",
    setCustomInstallPath: "set_custom_install_path",
    listDirectory: "list_directory",
    createDirectory: "create_directory",
    getBrowseableDevices: "get_browseable_devices",
    // Playtime (PlaytimeRPCMixin)
    notifyGameLaunched: "notify_game_launched",
    notifyGameStopped: "notify_game_stopped",
    getPlaytime: "get_playtime",
    // Action dispatcher (ActionRPCMixin) — bidirectional bridge
    dispatchUnifideckAction: "dispatch_unifideck_action",
    // Manual cloud-save status / pull / push (CloudSaveRPCMixin)
    getCloudSaveStatus: "get_cloud_save_status",
    cloudSavePull: "cloud_save_pull",
    cloudSavePush: "cloud_save_push",
    setGameSavePath: "set_game_save_path",
    // Launch executable override (ExecutableRPCMixin)
    listGameExecutables: "list_game_executables",
    setGameExecutable: "set_game_executable",
    resetGameExecutable: "reset_game_executable",
    // Achievements (AchievementsRPCMixin) — GOG display + last-session summary
    getGameAchievements: "get_game_achievements",
    getLastSessionAchievements: "get_last_session_achievements",
    // Observability (ObservabilityRPCMixin) — event bridge + diagnostics
    subscribeReplay: "subscribe_replay",
    getLauncherToasts: "get_launcher_toasts",
    captureLogs: "capture_logs",
    // Account switch + migration (AccountRPCMixin)
    checkAccountSwitch: "check_account_switch",
    migrateAccountData: "migrate_account_data",
    // Plugin self-update (UpdaterRPCMixin)
    checkPluginUpdate: "check_plugin_update",
    getAvailableVersions: "get_available_versions",
    forceCheckPluginUpdate: "force_check_plugin_update",
    forceGetAvailableVersions: "force_get_available_versions",
    logUpdateEvent: "log_update_event",
};

const Ctx$5 = SP_REACT.createContext(null);
/** Read the persisted preference synchronously for the first
 *  render. Defaults to "auto" so a fresh install shows
 *  "Auto-detect" selected. */
function readStoredPreference() {
    try {
        return localStorage.getItem(LANG_STORAGE_KEY) || AUTO_DETECT$1;
    }
    catch {
        return AUTO_DETECT$1;
    }
}
/** Apply a preference to i18next, resolving "auto" to the
 *  detected tag (i18next has no "auto" bundle). */
async function applyPreferenceToI18n(pref) {
    const tag = pref === AUTO_DETECT$1 ? resolveAutoLanguage() : pref;
    if (instance.language !== tag) {
        await instance.changeLanguage(tag);
    }
}
/**
 * Provider that owns the active UI language *preference*. Reads
 * it from localStorage, reconciles with the backend on mount,
 * then propagates user-initiated changes to i18next, localStorage
 * and the backend.
 */
const LocaleProvider = ({ children }) => {
    const [locale, setLocaleState] = SP_REACT.useState(readStoredPreference);
    // Set once the user picks a language, so the async mount fetch
    // below never clobbers an in-flight selection with a stale
    // backend value.
    const userTouchedRef = SP_REACT.useRef(false);
    const setMut = useRPCMutation(rpcRoutes.setLanguagePreference);
    // Reconcile with the durable backend preference once, on mount.
    // The bootstrap path that sets i18next isn't awaited, so the
    // backend is the authority for what the user last chose.
    SP_REACT.useEffect(() => {
        let cancelled = false;
        void (async () => {
            try {
                // get_language_preference returns the standard
                // {success, error, data} envelope — unwrap to the {locale}
                // payload (a raw `call` does NOT unwrap, unlike useRPC).
                const raw = await call(rpcRoutes.getLanguagePreference);
                const r = unwrapRpcEnvelope(raw, {
                    route: rpcRoutes.getLanguagePreference,
                    throwing: false,
                });
                const pref = r?.locale || AUTO_DETECT$1;
                if (cancelled || userTouchedRef.current)
                    return;
                await applyPreferenceToI18n(pref);
                try {
                    localStorage.setItem(LANG_STORAGE_KEY, pref);
                }
                catch {
                    // ignore quota/availability errors
                }
                if (!cancelled && !userTouchedRef.current)
                    setLocaleState(pref);
            }
            catch {
                // Backend not ready — keep the localStorage-derived value.
            }
        })();
        return () => {
            cancelled = true;
        };
    }, []);
    /** Set locale preference ("auto" or a concrete tag). */
    const setLocale = SP_REACT.useCallback(async (pref) => {
        userTouchedRef.current = true;
        await applyPreferenceToI18n(pref);
        try {
            localStorage.setItem(LANG_STORAGE_KEY, pref);
        }
        catch {
            // ignore quota/availability errors
        }
        setLocaleState(pref);
        await setMut.mutate(pref); // Persist
    }, [setMut]);
    const value = {
        locale,
        loading: false,
        setLocale,
    };
    return SP_JSX.jsx(Ctx$5.Provider, { value: value, children: children });
};
/**
 * Access the LocaleContext value. Throws if used
 * outside `<LocaleProvider>`.
 *
 * @throws Error when the provider is missing.
 */
function useLocale() {
    const v = SP_REACT.useContext(Ctx$5);
    if (!v)
        throw new Error("useLocale called outside <LocaleProvider>");
    return v;
}

/**
 * Bidirectional bridge to the backend EventBus.
 *
 * Backend emits events on its in-process Python EventBus.
 * Decky doesn't expose a server-push channel, so we use
 * `subscribe_replay(events)` which returns a snapshot of the
 * recent buffered events with their full kwargs payload. We
 * deduplicate by timestamp on the frontend side ; any event
 * we've already dispatched is filtered out.
 *
 * Reverse direction : `dispatch_unifideck_action(uri)` lets
 * the frontend trigger a backend action (auth, retry-sync,
 * refresh-library, ...). The URI form is
 * `unifideck://<verb>[/arg1/arg2]`. Toast/modal payloads
 * coming through events embed an optional `action` block
 * which the UI exposes as a button — clicking it dispatches
 * the corresponding URI back to the backend.
 *
 * Polling cadence is adaptive : 250ms when an operation is
 * in flight (events arriving), 2s when idle.
 */
const POLL_FAST_MS = 250;
const POLL_SLOW_MS = 2000;
/** All events the frontend ever cares about. The backend
 *  buffers each event type up to its capacity (50 for
 *  PROGRESS, 20 for state changes — see backend EventReplay
 *  defaults). Asking for an event type the backend doesn't
 *  buffer is safe : we get an empty list. */
const WATCHED_EVENTS = [
    "store_auth_started",
    "store_auth_complete",
    "store_auth_failed",
    "store_logout",
    "store_registered",
    "sync_started",
    "sync_progress",
    "sync_complete",
    "sync_failed",
    "sync_cancelled",
    "sync_skipped",
    "post_sync_phase_changed",
    "metadata_backfill_complete",
    "shortcut_reconcile_complete",
    "shortcut_install_state_changed",
    "download_queued",
    "download_started",
    "download_progress",
    "download_complete",
    "download_failed",
    "download_cancelled",
    // The backend emits this after bootstrapping a Ubisoft per-game prefix to
    // ask the frontend to RunGame the UPC shortcut. It MUST be polled here or
    // the download-store handler never fires and the install hangs forever on
    // "Installing Ubisoft Connect" (the rest of the chain — RunGame → launcher
    // → UPC — works; this allowlist omission was the whole bug).
    "ubisoft_install_launch_requested",
    "game_installed",
    "game_uninstalled",
    "game_update_available",
    "game_launched",
    "game_stopped",
    "cloud_sync_down_complete",
    "cloud_sync_down_failed",
    "cloud_sync_up_complete",
    "cloud_sync_up_failed",
    "store_error",
    "launcher_stage",
    "circuit_state_changed",
];
/** Imperative events that *do something* when dispatched (here: RunGame →
 *  open UPC) rather than just updating idempotent UI state. They must NOT be
 *  re-fired from the backend's replay backlog on a fresh load — otherwise a
 *  Steam restart relaunches UPC once per buffered event. They're primed past
 *  (watermark advanced, not dispatched) on the first poll after load; events
 *  emitted live during the session still fire normally. */
const IMPERATIVE_EVENTS = new Set(["ubisoft_install_launch_requested"]);
/** Sync-lifecycle events describe a sync that was already underway or
 *  finished in a PRIOR session. ``SteamRestartModal`` only restarts the
 *  Steam *client*; the Decky backend (and its in-memory replay buffer)
 *  keeps running, so on the next load these would replay from timestamp 0
 *  on the first poll — resurrecting the progress bar (stale ``sync_progress``)
 *  and re-showing the restart modal (``sync_started`` re-arms
 *  ``_observedActiveSync`` so the replayed ``shortcut_reconcile_complete``
 *  fires the prompt again). The authoritative restore is
 *  ``syncStore.start()`` → ``get_sync_progress``, so prime past these on the
 *  first poll after load; events emitted live during the session still fire
 *  normally (their timestamps exceed the watermark). */
const STALE_ON_RELOAD_EVENTS = new Set([
    "sync_started",
    "sync_progress",
    "sync_complete",
    "sync_failed",
    "sync_cancelled",
    "sync_skipped",
    "post_sync_phase_changed",
    "shortcut_reconcile_complete",
]);
/** Extract the records array from whatever the backend sent —
 *  either a raw list or the `{success, error, data}` envelope.
 *  Returns `[]` for any unexpected shape so callers don't crash. */
function extractRecords(raw) {
    // Delegate envelope unwrapping to the shared helper so the
    // semantics stay aligned with `useRPC` / `AuthDispatcher`.
    const unwrapped = unwrapRpcEnvelope(raw, {
        route: "subscribe_replay",
        throwing: false,
    });
    if (Array.isArray(unwrapped))
        return unwrapped;
    return [];
}
/** Event bus client impl. */
class EventBusClientImpl {
    constructor() {
        this.subscribers = new Map();
        this.lastSeenTimestamp = 0;
        /** False until the first poll after (re)load completes. While false, the
         *  backend's whole replay buffer reads as "fresh", so we skip dispatching
         *  imperative events (see IMPERATIVE_EVENTS) to avoid re-firing stale
         *  side effects from a prior session. */
        this.primed = false;
        this.timer = null;
        this.currentInterval = POLL_SLOW_MS;
    }
    /** Subscribe to a backend event by name. Returns the
     *  unsubscribe function for cleanup on unmount. */
    subscribe(name, handler) {
        let set = this.subscribers.get(name);
        if (!set) {
            set = new Set();
            this.subscribers.set(name, set);
        }
        set.add(handler);
        this.ensurePolling();
        return () => {
            set?.delete(handler);
            if (set?.size === 0)
                this.subscribers.delete(name);
            if (this.subscribers.size === 0)
                this.stopPolling();
        };
    }
    /** Bump the polling cadence to fast — call when starting
     *  an operation that will emit events imminently (auth,
     *  install, sync). Auto-decays back to slow once no events
     *  arrive for one tick. */
    bumpToFast() {
        this.currentInterval = POLL_FAST_MS;
    }
    /** Dispatch an action URI to the backend. Convenience
     *  wrapper around `dispatch_unifideck_action`. */
    async dispatchAction(verb, ...args) {
        const path = args.length
            ? "/" + args.map(encodeURIComponent).join("/")
            : "";
        const uri = `unifideck://${verb}${path}`;
        return call(rpcRoutes.dispatchUnifideckAction, uri);
    }
    /**
     * Start the polling loop if at least one handler
     * is registered and no loop is already running.
     * Idempotent — safe to call from every `on()`.
     */
    ensurePolling() {
        if (this.timer != null)
            return;
        this.scheduleNext();
    }
    /**
     * Stop the polling loop. Called when the last
     * handler is removed via `off()`. Pending timers
     * are cleared so no late dispatch happens after
     * teardown.
     */
    stopPolling() {
        if (this.timer != null) {
            clearTimeout(this.timer);
            this.timer = null;
        }
    }
    /**
     * Arm the next `pollOnce()` invocation. The
     * interval grows under back-pressure (no events
     * received) and resets to the floor on activity,
     * keeping the poll cheap when idle.
     */
    scheduleNext() {
        this.timer = setTimeout(() => {
            void this.pollOnce();
        }, this.currentInterval);
    }
    /**
     * Drain the backend event queue once, fanning
     * each event out to its registered handlers.
     * Network errors are swallowed and logged ; they
     * never propagate to handlers.
     */
    async pollOnce() {
        try {
            const raw = await call(rpcRoutes.subscribeReplay, WATCHED_EVENTS);
            // Backend wraps every RPC response in `{success, error, data}`
            // via `@auto_wrap_rpc_methods`. `useRPC` unwraps it for
            // component callers, but we call `call()` directly here, so
            // we have to unwrap manually. Tolerate both shapes.
            const records = extractRecords(raw);
            // Dedup : keep only events strictly newer than the last
            // we processed. Sort ascending so handlers see them in
            // emission order.
            const fresh = records
                .filter((r) => r.timestamp > this.lastSeenTimestamp)
                .sort((a, b) => a.timestamp - b.timestamp);
            // On the first poll after a (re)load the watermark is 0, so the whole
            // backend replay buffer is "fresh". Most state events are harmless to
            // replay (idempotent UI updates), but two classes are not and must be
            // primed past instead of fired: IMPERATIVE_EVENTS re-run a side effect
            // (RunGame → UPC), and STALE_ON_RELOAD_EVENTS re-animate a sync that
            // already finished (stuck progress bar + repeating restart modal). Both
            // still advance the watermark so they're never seen again; events emitted
            // live during the session fire normally.
            for (const r of fresh) {
                if (!this.primed &&
                    (IMPERATIVE_EVENTS.has(r.event) ||
                        STALE_ON_RELOAD_EVENTS.has(r.event)))
                    continue;
                this.dispatch(r);
            }
            this.primed = true;
            if (records.length > 0) {
                this.lastSeenTimestamp = Math.max(this.lastSeenTimestamp, ...records.map((r) => r.timestamp));
            }
            // Adaptive cadence : speed up when events arrive,
            // slow down when the backend is quiet.
            this.currentInterval = fresh.length > 0 ? POLL_FAST_MS : POLL_SLOW_MS;
        }
        catch (e) {
            console.warn("[EventBusClient] poll error:", e);
            this.currentInterval = POLL_SLOW_MS;
        }
        finally {
            if (this.subscribers.size > 0)
                this.scheduleNext();
            else
                this.timer = null;
        }
    }
    /**
     * Invoke every handler registered for the given
     * event name, isolating each call so a throwing
     * handler cannot break the others. Mirrors the
     * backend EventBus semantics described in the
     * architecture doc §3.2.
     */
    dispatch(record) {
        const handlers = this.subscribers.get(record.event);
        if (!handlers)
            return;
        for (const h of handlers) {
            try {
                h(record.kwargs);
            }
            catch (e) {
                console.error(`[EventBusClient] handler for ${record.event} threw:`, e);
            }
        }
    }
}
/** Singleton — process-global by nature. */
const EventBusClient = new EventBusClientImpl();
/** React hook — subscribe to a backend event for the
 *  lifetime of the component. The `deps` array follows the
 *  standard useEffect convention. */
function useEventBus(name, handler, deps = []) {
    const handlerRef = SP_REACT.useRef(handler);
    handlerRef.current = handler;
    SP_REACT.useEffect(() => {
        /** Stable. */
        const stable = (payload) => handlerRef.current(payload);
        return EventBusClient.subscribe(name, stable);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [name, ...deps]);
}

/**
 * store-info-store — boot-time cache for registered store info.
 *
 * Replaces the `useRPCQuery(getStoreInfos)` call that previously
 * ran on every QAM mount inside `<StoreProvider>`. The store:
 *
 *   1. Fetches `get_store_infos` once at boot.
 *   2. Subscribes to `STORE_REGISTERED` to auto-refresh when a
 *      new store connector is hot-loaded.
 *   3. Exposes a `refetch()` for manual refresh.
 *
 * React components subscribe via `useSyncExternalStore`.
 */
class StoreInfoStoreImpl {
    constructor() {
        this._snapshot = {
            stores: [],
            loading: true,
            error: null,
        };
        this._listeners = new Set();
        this._unsubs = [];
        // ── useSyncExternalStore API ──────────────────────────
        this.getSnapshot = () => this._snapshot;
        this.subscribe = (listener) => {
            this._listeners.add(listener);
            return () => this._listeners.delete(listener);
        };
    }
    /** Start the store — initial fetch + STORE_REGISTERED sub. */
    start() {
        void this._fetch();
        this._unsubs.push(EventBusClient.subscribe("store_registered", () => {
            void this._fetch();
        }));
    }
    /** Stop all subscriptions. */
    stop() {
        for (const unsub of this._unsubs)
            unsub();
        this._unsubs = [];
    }
    /** Re-fetch store infos from the backend. */
    async refetch() {
        await this._fetch();
    }
    // ── Internals ─────────────────────────────────────────
    async _fetch() {
        try {
            const raw = await call(rpcRoutes.getStoreInfos);
            const data = unwrapRpcEnvelope(raw, {
                route: rpcRoutes.getStoreInfos,
                throwing: false,
            });
            this._snapshot = {
                stores: Array.isArray(data) ? data : [],
                loading: false,
                error: null,
            };
            this._emit();
        }
        catch (e) {
            this._snapshot = {
                stores: this._snapshot.stores,
                loading: false,
                error: e instanceof Error ? e : new Error(String(e)),
            };
            this._emit();
        }
    }
    _emit() {
        for (const listener of this._listeners)
            listener();
    }
}
/** Singleton — started at boot from `definePlugin`. */
const storeInfoStore = new StoreInfoStoreImpl();

const Ctx$4 = SP_REACT.createContext(null);
/**
 * Provider that exposes registered stores to descendants.
 * State comes from the boot-time `storeInfoStore` singleton.
 */
const StoreProvider = ({ children }) => {
    const { stores, loading, error } = SP_REACT.useSyncExternalStore(storeInfoStore.subscribe, storeInfoStore.getSnapshot);
    const refetch = SP_REACT.useCallback(async () => {
        await storeInfoStore.refetch();
    }, []);
    const value = {
        stores,
        loading,
        error,
        refetch,
    };
    return SP_JSX.jsx(Ctx$4.Provider, { value: value, children: children });
};
/**
 * Access the StoreContext value. Throws if called
 * outside the provider — that always indicates a
 * bug in the component tree, never a recoverable
 * runtime condition.
 *
 * @throws Error when the surrounding tree is missing
 *   `<StoreProvider>`.
 */
function useStores() {
    const v = SP_REACT.useContext(Ctx$4);
    if (!v) {
        throw new Error("useStores called outside <StoreProvider>");
    }
    return v;
}

/**
 * ProtonDB & Steam Deck compat cache.
 *
 * Synchronous in-memory lookup populated once from the backend
 * `get_protondb_cache` RPC. The cache feeds two surfaces :
 *  - the `deckCompat` library-tab filter (synchronous)
 *  - the Deck Verified / ProtonDB badges in the game-info panel
 *
 * Ported from staging:src/tabs/protondb.ts. Uses `call` directly
 * because the loader runs once at module-init time, outside any
 * React tree (no hook context available).
 */
/** Title-keyed cache (Epic/GOG/Amazon/Ubisoft/Microsoft games). */
const compatCache = new Map();
/** AppId-keyed cache (native Steam games). */
const protonDBCache = new Map();
let cacheLoadedFromBackend = false;
function normalizeTitle(title) {
    return title.toLowerCase().trim();
}
/**
 * Load the compat cache from the backend once. Idempotent — safe to
 * call multiple times during boot. Catches every error so a backend
 * outage degrades to "no badges, deckCompat tab empty" rather than
 * crashing module init.
 */
async function loadCompatCacheFromBackend(force = false) {
    if (cacheLoadedFromBackend && !force)
        return;
    try {
        const raw = await call(rpcRoutes.getProtondbCache);
        if (!raw || typeof raw !== "object") {
            cacheLoadedFromBackend = true;
            return;
        }
        // A forced reload (post-sync) repopulates from scratch so entries
        // dropped upstream don't linger.
        if (force) {
            compatCache.clear();
            protonDBCache.clear();
        }
        for (const [key, entry] of Object.entries(raw)) {
            if (!entry || typeof entry !== "object")
                continue;
            const tier = (entry.protondb_tier ?? null);
            const deckVerified = (entry.deck_status ??
                "unknown");
            const steamAppId = typeof entry.appid === "number" ? entry.appid : Number(key);
            const ts = Date.now();
            const titleKey = entry.title ? normalizeTitle(entry.title) : null;
            const value = {
                tier,
                deckVerified,
                steamAppId: Number.isFinite(steamAppId) ? steamAppId : null,
                timestamp: ts,
            };
            if (titleKey)
                compatCache.set(titleKey, value);
            if (Number.isFinite(steamAppId) && tier) {
                protonDBCache.set(steamAppId, { tier, timestamp: ts });
            }
        }
        cacheLoadedFromBackend = true;
        console.log(`[Unifideck] Loaded ${compatCache.size} title + ${protonDBCache.size} appId compat entries`);
    }
    catch (e) {
        console.error("[Unifideck] loadCompatCacheFromBackend failed", e);
    }
}
/** Synchronous ProtonDB tier lookup by Steam appId (native games). */
function getCachedRating(appId) {
    return protonDBCache.get(appId)?.tier ?? null;
}
/** Synchronous compat info lookup by game title (non-Steam games). */
function getCachedCompatByTitle(title) {
    const cached = compatCache.get(normalizeTitle(title));
    if (!cached)
        return null;
    return {
        tier: cached.tier,
        deckVerified: cached.deckVerified,
        steamAppId: cached.steamAppId,
    };
}
/**
 * "Great on Deck" = Verified / Playable on Deck, OR Native / Platinum on
 * ProtonDB. Gold-only without Deck verification fails — matches staging.
 */
function meetsGreatOnDeckCriteria(compat) {
    if (!compat)
        return false;
    if (compat.deckVerified === "verified" ||
        compat.deckVerified === "playable") {
        return true;
    }
    if (compat.tier === "native" || compat.tier === "platinum")
        return true;
    return false;
}

/**
 * EventBus event names — mirror of backend `core/types/events.py`.
 *
 * Names and lowercase string values match the backend Events
 * enum exactly. The frontend subscribes via `subscribe_replay`
 * (see `api/event-bus-client.ts`) ; these are the wire strings
 * the backend emits, so a typo here breaks the bridge silently.
 *
 * Single source of truth on the frontend. Components never
 * reference event strings directly — they import `Events`.
 */
const Events = {
    STORE_AUTH_COMPLETE: "store_auth_complete",
    STORE_AUTH_FAILED: "store_auth_failed",
    SYNC_COMPLETE: "sync_complete",
    SYNC_FAILED: "sync_failed",
    SYNC_CANCELLED: "sync_cancelled",
    // Fired by the background Metacritic backfill once its long-tail
    // metacritic.com lookups have landed (after the progress bar hit
    // 100%). The overview-enrichment layer re-reads facets on this so
    // newly-backfilled scores surface in Steam's native Sort-by-Metacritic
    // without a manual resync/restart.
    METADATA_BACKFILL_COMPLETE: "metadata_backfill_complete",
    // Emitted by ShortcutService when an existing shortcut's install
    // state flips (post-install/uninstall, no shortcut recreation).
    // Payload: { store, store_game_id, app_id, installed,
    //            exe_path, install_path }. Drives unifideckGameCache
    //  refresh so the GOG tab and detail-page UI react immediately.
    SHORTCUT_INSTALL_STATE_CHANGED: "shortcut_install_state_changed",
    GAME_STOPPED: "game_stopped",
    // Cloud-save sync (CloudSaveService)
    CLOUD_SYNC_DOWN_COMPLETE: "cloud_sync_down_complete",
    CLOUD_SYNC_DOWN_FAILED: "cloud_sync_down_failed",
    CLOUD_SYNC_UP_COMPLETE: "cloud_sync_up_complete",
    CLOUD_SYNC_UP_FAILED: "cloud_sync_up_failed"};

/**
 * Library Facets — per-shortcut enrichment data for Steam's native
 * library Sort menu + Library Filters, plus shortcut-keyed
 * Great-on-Deck resolution.
 *
 * Our custom tabs render through Steam's own grid, which sorts and
 * filters purely on `AppOverview` field values. Non-Steam shortcuts
 * have those fields empty, so this module pulls a per-shortcut
 * `FacetRecord` from the backend (`get_overview_enrichment`, a pure
 * reshape of the metadata/compat caches) and the overview-enrichment
 * layer writes them onto the live overviews.
 *
 * The same records carry `protondb_tier` / `deck_status` keyed by the
 * **shortcut** AppID, so Great-on-Deck no longer depends on fuzzy
 * title matching against the compat cache.
 *
 * Backend keys every record under BOTH the signed and unsigned 32-bit
 * forms of the shortcut AppID; we store whatever it sends, so a lookup
 * by either form hits.
 */
const facetByAppId = new Map();
let loaded = false;
/** Synchronous facet lookup by AppID (signed or unsigned). */
function getFacet(appId) {
    return facetByAppId.get(appId) ?? null;
}
/**
 * Derive a `GameCompatInfo` for Great-on-Deck from the shortcut-keyed
 * facet — no title matching. Returns null when this AppID has no
 * facet (caller falls back to the title-keyed compat cache).
 *
 * `deck_status` strings ("verified"/"playable"/"unsupported"/"unknown")
 * line up 1:1 with `DeckVerifiedStatus`.
 */
function getCompatByShortcutAppId(appId) {
    const f = facetByAppId.get(appId);
    if (!f)
        return null;
    return {
        tier: f.protondb_tier ?? null,
        deckVerified: f.deck_status || "unknown",
        steamAppId: f.steam_app_id || null,
    };
}
/**
 * Load the per-shortcut enrichment map from the backend. Idempotent
 * unless `force` is passed (used to refresh after a library sync).
 * Catches every error so a backend outage degrades to "no enrichment"
 * rather than breaking plugin boot.
 */
async function loadFacets(force = false) {
    if (loaded && !force)
        return;
    try {
        const raw = await call(rpcRoutes.getOverviewEnrichment);
        const map = unwrapRpcEnvelope(raw, { route: rpcRoutes.getOverviewEnrichment });
        const entries = Object.entries(map ?? {});
        // A forced reload during a library sync can race the backend: the
        // metadata/compat caches are briefly mid-rebuild and the RPC
        // returns empty. Don't wipe good enrichment in that window — keep
        // what we have and let the next sync-completed refresh fill it.
        if (entries.length === 0 && facetByAppId.size > 0) {
            return;
        }
        facetByAppId.clear();
        for (const [key, rec] of entries) {
            const id = Number(key);
            if (!Number.isNaN(id) && rec)
                facetByAppId.set(id, rec);
        }
        loaded = true;
        console.log(`[Unifideck] Loaded overview enrichment for ${facetByAppId.size} shortcut-id keys`);
    }
    catch (e) {
        console.error("[Unifideck] loadFacets failed", e);
    }
}

/**
 * Library tab filters — pure predicates over `SteamAppOverview`.
 *
 * Powers the custom Unifideck library tabs. Each filter receives an
 * app overview and returns whether the app belongs in the tab. The
 * Unifideck game cache (store-of-record for non-Steam shortcuts we
 * manage) and the third-party shortcut allow-list are populated by
 * `LibraryContext` from `get_all_unifideck_games`.
 *
 * Ported from `staging:src/tabs/filters.ts`. The module-level caches
 * mirror staging's design — keeping them outside React lets the
 * Steam library-patch hook call `runFilters` from a non-component
 * context without prop-drilling.
 */
const NON_STEAM_APP_TYPE$1 = 1073741824;
const DECK_VERIFIED = 3;
/** Stored in both signed and unsigned forms — Steam returns the
 *  appid signed in some surfaces and unsigned in others, and the
 *  filter functions can't predict which. */
const unifideckGameCache = new Map();
/** Reverse index: ``"<store>:<store_game_id>"`` → shortcut appId.
 *  Lets callers that only hold a store/game-id pair (e.g. the
 *  downloads queue) resolve back to the Steam shortcut appId for
 *  launch. Populated alongside ``unifideckGameCache``. */
const unifideckAppIdByStoreGame = new Map();
/** Resolve a Steam shortcut appId from a store + store-game-id
 *  pair. Returns null when the game isn't in the cache yet. */
function resolveAppIdFromStoreGame(store, gameId) {
    return unifideckAppIdByStoreGame.get(`${store}:${gameId}`) ?? null;
}
/** Version bumped on every per-app status change — patchers read
 *  this to know when to remount React subtrees. */
const gameStateVersion = new Map();
function variantIds(appId) {
    const ids = new Set([appId]);
    if (appId < 0)
        ids.add(appId + 0x100000000);
    if (appId >= 0 && appId > 0x7fffffff)
        ids.add(appId - 0x100000000);
    return [...ids];
}
function updateUnifideckCache(games) {
    unifideckGameCache.clear();
    unifideckAppIdByStoreGame.clear();
    for (const g of games) {
        const entry = {
            store: g.store,
            isInstalled: g.isInstalled,
            steamAppId: g.steamAppId,
            storeGameId: g.storeGameId,
        };
        for (const id of variantIds(g.appId))
            unifideckGameCache.set(id, entry);
        if (g.storeGameId) {
            unifideckAppIdByStoreGame.set(`${g.store}:${g.storeGameId}`, g.appId);
        }
    }
}
function updateSingleGameStatus(g) {
    const existing = unifideckGameCache.get(g.appId);
    const entry = {
        store: g.store,
        isInstalled: g.isInstalled,
        steamAppId: existing?.steamAppId ?? g.steamAppId,
        storeGameId: g.storeGameId ?? existing?.storeGameId,
    };
    for (const id of variantIds(g.appId))
        unifideckGameCache.set(id, entry);
    if (existing &&
        existing.isInstalled === g.isInstalled &&
        existing.store === g.store)
        return;
    const next = (gameStateVersion.get(g.appId) ?? 0) + 1;
    for (const id of variantIds(g.appId))
        gameStateVersion.set(id, next);
    window.dispatchEvent(new CustomEvent("unifideck-game-state-changed", {
        detail: { appId: g.appId, isInstalled: g.isInstalled, store: g.store },
    }));
}
function isUnifideckGame(appId) {
    return unifideckGameCache.has(appId);
}
/** Resolve a shortcut's ``{ store, storeGameId, isInstalled }`` from its
 *  Steam appId, or ``null`` if it isn't a known Unifideck shortcut. Used by
 *  the "Change executable" context-menu item to gate + address the RPCs. */
function getUnifideckGame(appId) {
    const cached = unifideckGameCache.get(appId);
    if (!cached)
        return null;
    return {
        store: cached.store,
        storeGameId: cached.storeGameId,
        isInstalled: cached.isInstalled,
    };
}
function getStoreForApp(appId, appType) {
    const cached = unifideckGameCache.get(appId);
    if (cached)
        return cached.store;
    if (appType === NON_STEAM_APP_TYPE$1)
        return null;
    return "steam";
}
function getInstalledStatus(appId, _appType, steamInstalledFlag) {
    return unifideckGameCache.get(appId)?.isInstalled ?? steamInstalledFlag;
}
const filterFunctions = {
    all: (_p, app) => {
        if (app.app_type !== NON_STEAM_APP_TYPE$1)
            return true;
        return isUnifideckGame(app.appid);
    },
    installed: (params, app) => {
        const isInstalled = getInstalledStatus(app.appid, app.app_type, app.installed);
        const match = params.installed ? isInstalled : !isInstalled;
        if (params.installed && app.app_type === NON_STEAM_APP_TYPE$1) {
            if (unifideckGameCache.has(app.appid)) {
                return match;
            }
            return false;
        }
        return match;
    },
    platform: (params, app) => {
        if (params.platform === "all")
            return true;
        if (params.platform === "steam")
            return app.app_type !== NON_STEAM_APP_TYPE$1;
        return app.app_type === NON_STEAM_APP_TYPE$1;
    },
    store: (params, app) => {
        if (params.store === "all")
            return true;
        const store = getStoreForApp(app.appid, app.app_type);
        if (store === null)
            return false;
        return store === params.store;
    },
    deckCompat: (_p, app) => {
        if (app.steam_deck_compat_category === DECK_VERIFIED)
            return true;
        const cached = unifideckGameCache.get(app.appid);
        if (cached) {
            // Prefer the shortcut-keyed facet compat — the backend already
            // resolved this shortcut → real-Steam-AppID → compat via the
            // centralised title matcher, so no fuzzy lookup against the
            // lossy ``display_name`` is needed here.
            const facetCompat = getCompatByShortcutAppId(app.appid);
            if (facetCompat)
                return meetsGreatOnDeckCriteria(facetCompat);
            // Fallback: title-keyed compat for shortcuts the metadata phase
            // never mapped to a Steam AppID (no facet yet).
            const title = app.display_name || "";
            if (!title)
                return false;
            return meetsGreatOnDeckCriteria(getCachedCompatByTitle(title));
        }
        // Native Steam game (not a Unifideck shortcut): ``app.appid`` is a
        // real Steam AppID, so the appid-keyed ProtonDB rating applies.
        const tier = getCachedRating(app.appid);
        return tier === "native" || tier === "platinum";
    },
    nonSteam: (_p, app) => {
        if (app.app_type !== NON_STEAM_APP_TYPE$1)
            return false;
        if (unifideckGameCache.has(app.appid))
            return false;
        return true;
    },
};
function getHiddenAppIds() {
    try {
        const cs = window.collectionStore;
        const hidden = cs?.GetCollection?.("hidden");
        if (hidden?.allApps)
            return new Set(hidden.allApps.map((a) => a.appid));
    }
    catch (e) {
        console.error("[Unifideck] hidden collection lookup failed", e);
    }
    return new Set();
}
function isGameHidden(appId) {
    return getHiddenAppIds().has(appId);
}
function runFilter(filter, app) {
    const fn = filterFunctions[filter.type];
    if (!fn)
        return true;
    return fn(filter.params, app);
}
function runFilters(filters, app) {
    if (isGameHidden(app.appid))
        return false;
    return filters.every((f) => runFilter(f, app));
}
let storeCountSink = null;
let cacheLoadStarted = false;
let cacheLoaded = false;
// Bounded auto-retry for a failed initial load. A transient
// ``get_all_unifideck_games`` failure used to latch ``cacheLoaded =
// true`` with an empty cache, so every Unifideck shortcut failed
// ``isUnifideckGame`` and all library tabs showed 0 until the next
// full sync (UD-008 / UD-043 "synced but nothing shows"). We now only
// mark the cache authoritative on success and retry a handful of times
// with backoff, then give up (so the user's own shortcuts' native UI
// isn't hidden forever if the backend is genuinely down).
const _CACHE_RETRY_MAX = 5;
const _CACHE_RETRY_BASE_MS = 1500;
let cacheRetryCount = 0;
let cacheRetryTimer = null;
/** Whether the first ``loadUnifideckCache`` attempt has completed.
 *  Synchronous callers (e.g. the App-Details patch) stay optimistic
 *  before this flips so a Unifideck game opened on cold boot isn't
 *  mistaken for a plain shortcut; once true, ``isUnifideckGame`` is
 *  authoritative. */
function isUnifideckCacheLoaded() {
    return cacheLoaded;
}
/** Register a callback to receive per-store game counts. The
 *  tab manager uses this to drive ``shouldShowTab``. */
function setStoreCountSink(sink) {
    storeCountSink = sink;
}
function isNonSteamStore(s) {
    return s !== undefined && s !== "steam";
}
/** Fetch the unified game list from the backend and populate
 *  ``unifideckGameCache``. Idempotent — safe to call from both
 *  the eager plugin-init path and the QAM-mount path. */
async function loadUnifideckCache() {
    try {
        const raw = await call("get_all_unifideck_games");
        // Backend wraps every response in ``{success, error, data}``
        // — unwrap so we end up with the actual list of games.
        const games = unwrapRpcEnvelope(raw, {
            route: "get_all_unifideck_games",
        });
        const inputs = [];
        const counts = {
            epic: 0,
            gog: 0,
            amazon: 0,
            ubisoft: 0,
            microsoft: 0,
        };
        for (const g of games ?? []) {
            if (g.app_id == null)
                continue;
            if (!isNonSteamStore(g.store))
                continue;
            const steamAppId = typeof g.metadata?.steam_app_id === "number"
                ? g.metadata.steam_app_id
                : undefined;
            inputs.push({
                appId: g.app_id,
                store: g.store,
                isInstalled: Boolean(g.installed),
                steamAppId,
                storeGameId: g.store_game_id,
            });
            counts[g.store] += 1;
        }
        // SUCCESS: populate + mark authoritative. An empty-but-valid
        // response (0 games) legitimately marks the cache loaded — the
        // library is genuinely empty. Only an RPC *failure* must not latch.
        updateUnifideckCache(inputs);
        storeCountSink?.(counts);
        cacheLoaded = true;
        cacheRetryCount = 0;
        if (cacheRetryTimer !== null) {
            clearTimeout(cacheRetryTimer);
            cacheRetryTimer = null;
        }
    }
    catch (e) {
        // FAILURE: do NOT latch ``cacheLoaded`` or wipe the existing cache
        // (``updateUnifideckCache`` was never reached, so a prior good load
        // survives). Latching true here with an empty cache made every
        // library tab render 0 (UD-008/UD-043). Retry with backoff so a
        // transient failure self-heals without waiting for a full sync.
        console.error("[Unifideck] loadUnifideckCache failed", e);
        if (cacheRetryCount < _CACHE_RETRY_MAX) {
            cacheRetryCount += 1;
            const delay = _CACHE_RETRY_BASE_MS * cacheRetryCount;
            if (cacheRetryTimer !== null)
                clearTimeout(cacheRetryTimer);
            cacheRetryTimer = setTimeout(() => {
                cacheRetryTimer = null;
                void loadUnifideckCache();
            }, delay);
        }
        else {
            // Exhausted retries — accept the empty state so synchronous
            // callers stop treating every shortcut as a potential Unifideck
            // game (which would keep the user's own shortcuts' native UI
            // hidden). The next ``unifideck-sync-completed`` still retries.
            cacheLoaded = true;
        }
    }
}
/** Kick off the eager load + subscribe to sync-completed events
 *  for refresh. Idempotent — calls after the first are no-ops. */
function startUnifideckCacheAutoload() {
    if (cacheLoadStarted)
        return;
    cacheLoadStarted = true;
    void loadUnifideckCache();
    // Eager-load the compat cache + per-shortcut facet enrichment at
    // plugin init (not just when the QAM panel mounts) so the Steam
    // library's Great-on-Deck tab + native Sort/Filters have data on
    // first render in Gaming Mode, where the panel is never opened.
    void loadCompatCacheFromBackend();
    void loadFacets();
    window.addEventListener("unifideck-sync-completed", () => {
        // A fresh sync is a new chance for a previously-failed load —
        // reset the retry budget so a transient earlier failure doesn't
        // leave us out of retries.
        cacheRetryCount = 0;
        void loadUnifideckCache();
        // A fresh sync rebuilt the metadata/compat caches — refresh the
        // derived compat + facet data so badges/sort/filters reflect it.
        void loadCompatCacheFromBackend(true);
        void loadFacets(true);
    });
    // ShortcutService emits SHORTCUT_INSTALL_STATE_CHANGED on
    // post-install/uninstall — flip the per-app entry immediately so
    // the GOG tab and detail-page UI react without waiting for the
    // next full library reload.
    EventBusClient.subscribe(Events.SHORTCUT_INSTALL_STATE_CHANGED, (kw) => {
        const appId = kw.app_id;
        const store = kw.store;
        const installed = kw.installed;
        if (typeof appId !== "number")
            return;
        if (typeof store !== "string")
            return;
        if (typeof installed !== "boolean")
            return;
        if (!isNonSteamStore(store))
            return;
        updateSingleGameStatus({
            appId,
            store: store,
            isInstalled: installed,
        });
    });
}

const Ctx$3 = SP_REACT.createContext(null);
const LibraryProvider = ({ children }) => {
    const [ready, setReady] = SP_REACT.useState(() => unifideckGameCache.size > 0);
    const refresh = SP_REACT.useCallback(async () => {
        await loadUnifideckCache();
        await loadCompatCacheFromBackend();
        setReady(true);
    }, []);
    SP_REACT.useEffect(() => {
        // On mount, just check if the cache is already populated (from boot).
        // If not, load it now (first QAM open before boot finished).
        if (!ready) {
            void refresh();
        }
        const onSync = () => void refresh();
        window.addEventListener("unifideck-sync-completed", onSync);
        return () => window.removeEventListener("unifideck-sync-completed", onSync);
    }, [refresh, ready]);
    return SP_JSX.jsx(Ctx$3.Provider, { value: { ready, refresh }, children: children });
};

/**
 * auth-store — boot-time singleton for per-store auth status.
 *
 * Absorbs the `prefetchAuthStatus()` logic and the EventBus
 * subscriptions (`STORE_AUTH_COMPLETE`, `STORE_AUTH_FAILED`,
 * `STORE_LOGOUT`) that previously lived inside `<AuthProvider>`.
 *
 * The store starts at boot and keeps auth statuses current even
 * when the QAM panel is closed (e.g. during an OAuth flow that
 * completes in the browser while QAM is dismissed).
 */
// ── Helpers ─────────────────────────────────────────────
function parseStatuses(raw) {
    const data = unwrapRpcEnvelope(raw);
    const arr = Array.isArray(data) ? data : [];
    const map = {};
    for (const entry of arr) {
        if (entry && typeof entry === "object") {
            const e = entry;
            const id = e.store_id;
            if (id) {
                map[id] = e.available ? "connected" : "disconnected";
            }
        }
    }
    return map;
}
class AuthStoreImpl {
    constructor() {
        this._snapshot = { statuses: {}, loading: true };
        this._listeners = new Set();
        this._unsubs = [];
        // ── useSyncExternalStore API ──────────────────────────
        this.getSnapshot = () => this._snapshot;
        this.subscribe = (listener) => {
            this._listeners.add(listener);
            return () => this._listeners.delete(listener);
        };
    }
    /** Start the store — initial fetch + event subscriptions. */
    start() {
        // Initial fetch (replaces prefetchAuthStatus)
        void call(rpcRoutes.checkStoreStatus)
            .then((raw) => {
            this._setSnapshot({
                statuses: parseStatuses(raw),
                loading: false,
            });
        })
            .catch(() => {
            this._setSnapshot({ statuses: {}, loading: false });
        });
        this._unsubs.push(EventBusClient.subscribe("store_auth_complete", (payload) => {
            const store = payload.store;
            if (store)
                this._updateStatus(store, "connected");
        }));
        this._unsubs.push(EventBusClient.subscribe("store_auth_failed", (payload) => {
            const store = payload.store;
            if (store)
                this._updateStatus(store, "error");
        }));
        this._unsubs.push(EventBusClient.subscribe("store_logout", (payload) => {
            const store = payload.store;
            if (store)
                this._updateStatus(store, "disconnected");
        }));
    }
    /** Stop all subscriptions. */
    stop() {
        for (const unsub of this._unsubs)
            unsub();
        this._unsubs = [];
    }
    /** Synchronous status update — called by AuthContext when
     *  useStoreAuth reports success (bypasses EventBus race). */
    notifyConnected(store) {
        this._updateStatus(store, "connected");
    }
    /** Clear all statuses (called by logoutAll). */
    clearAll() {
        this._setSnapshot({ statuses: {}, loading: false });
    }
    /** Re-fetch statuses from the backend. */
    async refetch() {
        try {
            const raw = await call(rpcRoutes.checkStoreStatus);
            this._setSnapshot({
                statuses: parseStatuses(raw),
                loading: false,
            });
        }
        catch {
            /* best-effort */
        }
    }
    // ── Internals ─────────────────────────────────────────
    _updateStatus(store, status) {
        this._setSnapshot({
            statuses: { ...this._snapshot.statuses, [store]: status },
            loading: false,
        });
    }
    _setSnapshot(next) {
        this._snapshot = next;
        this._emit();
    }
    _emit() {
        for (const listener of this._listeners)
            listener();
    }
}
/** Singleton — started at boot from `definePlugin`. */
const authStore = new AuthStoreImpl();

const Ctx$2 = SP_REACT.createContext(null);
/**
 * Provider that tracks per-store auth status. State comes from
 * the boot-time `authStore` singleton; user-initiated mutations
 * stay in the React layer.
 */
const AuthProvider = ({ children }) => {
    const { statuses, loading } = SP_REACT.useSyncExternalStore(authStore.subscribe, authStore.getSnapshot);
    // RPC mutations
    const startMut = useRPCMutation(rpcRoutes.storeAuth);
    const logoutMut = useRPCMutation(rpcRoutes.storeAuth);
    const logoutAllMut = useRPCMutation(rpcRoutes.clearStoreAuths);
    const startAuth = SP_REACT.useCallback((store) => startMut.mutate(store, "start"), [startMut]);
    const logout = SP_REACT.useCallback(async (store) => {
        await logoutMut.mutate(store, "logout");
    }, [logoutMut]);
    const logoutAll = SP_REACT.useCallback(async () => {
        await logoutAllMut.mutate();
        authStore.clearAll();
    }, [logoutAllMut]);
    const notifyConnected = SP_REACT.useCallback((store) => {
        authStore.notifyConnected(store);
    }, []);
    const value = {
        statuses,
        loading,
        startAuth,
        logout,
        logoutAll,
        notifyConnected,
    };
    return SP_JSX.jsx(Ctx$2.Provider, { value: value, children: children });
};
/**
 * Access the AuthContext value. Throws if used
 * outside `<AuthProvider>`.
 *
 * @throws Error when the provider is missing.
 */
function useAuth() {
    const v = SP_REACT.useContext(Ctx$2);
    if (!v)
        throw new Error("useAuth called outside <AuthProvider>");
    return v;
}

/**
 * useSyncCooldown — manual-sync rate limiter.
 *
 * After a sync completes, the manual Sync button is disabled
 * for `cooldownMs` so users don't hammer the backend with
 * redundant runs. The cooldown is module-level so it survives
 * QAM dismounts (legacy behaviour from staging's
 * `LibrarySync.tsx` cooldown timer).
 *
 * Listens for `SYNC_COMPLETE` / `SYNC_FAILED` /
 * `SYNC_CANCELLED` to start the cooldown ; a 1-second
 * interval re-renders consumers so the countdown is visible.
 *
 * The cooldown duration is configurable at runtime via
 * `setSyncCooldownMs` — SyncContext calls it on mount with the
 * value from `get_sync_status().cooldown_ms` so power users can
 * tune it via the backend config without a code change. Default
 * matches staging (5s) to undo the 30s regression that ships in
 * for-pr-0.7 before this hook is wired through.
 */

const DEFAULT_COOLDOWN_MS = 5000;
/** Module-level so the cooldown survives QAM dismount/remount.
 *  Stores the unix-ms timestamp when the cooldown expires. */
let cooldownEndsAt = 0;
/** Active cooldown duration. Mutable via `setSyncCooldownMs`. */
let cooldownMs = DEFAULT_COOLDOWN_MS;
/** Override the active cooldown. Called once at SyncContext mount
 *  after fetching `get_sync_status().cooldown_ms` from the backend.
 *  Falls back to the default if the value is invalid. */
function setSyncCooldownMs(ms) {
    if (!Number.isFinite(ms) || ms < 0)
        return;
    cooldownMs = ms;
}
/** Read live cooldown state. */
function useSyncCooldown() {
    const [now, setNow] = SP_REACT.useState(Date.now);
    // Tick once a second so the countdown re-renders. Stops
    // ticking when the cooldown is over (saves a render/sec).
    SP_REACT.useEffect(() => {
        if (cooldownEndsAt <= now)
            return undefined;
        const id = setInterval(() => setNow(Date.now()), 1000);
        return () => clearInterval(id);
    }, [now]);
    // On any terminal sync event, arm the cooldown and trigger
    // a re-render so the timer immediately reflects the change.
    useEventBus(Events.SYNC_COMPLETE, () => {
        cooldownEndsAt = Date.now() + cooldownMs;
        setNow(Date.now());
    });
    useEventBus(Events.SYNC_FAILED, () => {
        cooldownEndsAt = Date.now() + cooldownMs;
        setNow(Date.now());
    });
    useEventBus(Events.SYNC_CANCELLED, () => {
        cooldownEndsAt = Date.now() + cooldownMs;
        setNow(Date.now());
    });
    const remainingMs = Math.max(0, cooldownEndsAt - now);
    return {
        canSync: remainingMs === 0,
        remainingSecs: Math.ceil(remainingMs / 1000),
    };
}

/**
 * Modal shown after operations that require Steam to
 * restart for changes to apply (shortcut creation, VDF
 * mutation). Offers a Restart-now button that calls
 * `SteamClient.User.StartRestart`.
 *
 * Title + description are derived from `reason`/`store` so a
 * single component covers sync, cleanup, and per-store flows.
 */
const SteamRestartModal = ({ closeModal, store, reason }) => {
    const { t } = useTranslation();
    const title = reason === "cleanup"
        ? t("confirmModals.steamRestartCleanupTitle")
        : t("restart.title");
    const description = reason === "cleanup"
        ? t("confirmModals.steamRestartCleanupDescription")
        : store
            ? t("confirmModals.steamRestartDescriptionStore", { store })
            : t("restart.body");
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: title, strDescription: description, strOKButtonText: t("restart.confirm"), strCancelButtonText: t("restart.later"), onOK: () => {
            // SteamClient.User.StartShutdown is observed-not-typed
            const sc = window.SteamClient;
            sc?.User?.StartShutdown?.(false);
            closeModal?.();
        }, onCancel: () => closeModal?.() }));
};

const PROGRESS_POLL_MS = 500;
class SyncStoreImpl {
    constructor() {
        this._snapshot = {
            progress: null,
            isSyncing: false,
            isCancelling: false,
        };
        this._listeners = new Set();
        this._unsubs = [];
        this._pollTimer = null;
        this._pendingPhases = new Set();
        this._observedActiveSync = false;
        this._pendingRestart = false;
        // ── useSyncExternalStore API ──────────────────────────
        this.getSnapshot = () => this._snapshot;
        this.subscribe = (listener) => {
            this._listeners.add(listener);
            return () => this._listeners.delete(listener);
        };
    }
    /** Start subscriptions and initial state restore. */
    start() {
        // Restore state — a sync may already be in flight from
        // before the frontend loaded.
        void this._pollOnce();
        this._unsubs.push(EventBusClient.subscribe("sync_started", (payload) => {
            this._observedActiveSync = true;
            // Seed the phase set from the backend's authoritative
            // ``registered_phases`` rather than a hardcoded list. The old
            // hardcoded {artwork,metadata,proton_meta} over-counted when the
            // real set was smaller (no CompatibilityService → no proton_meta),
            // so the set never drained and the Steam-restart modal never fired
            // (UD-006). Fall back to the legacy set for replay/older backends.
            // An artwork skip is drained by its own phase-done event.
            const phases = payload
                ?.registered_phases;
            this._pendingPhases = new Set(Array.isArray(phases) && phases.length
                ? phases
                : ["artwork", "metadata", "proton_meta"]);
            this._update({ isSyncing: true, isCancelling: false });
            EventBusClient.bumpToFast();
            this._startPolling();
        }));
        this._unsubs.push(EventBusClient.subscribe("sync_progress", (payload) => {
            this._update({ progress: payload });
        }));
        this._unsubs.push(EventBusClient.subscribe("sync_complete", () => {
            this._update({ isCancelling: false });
            // Do NOT set isSyncing=false here — wait for post-sync phases.
            window.dispatchEvent(new CustomEvent("unifideck-sync-completed"));
        }));
        this._unsubs.push(EventBusClient.subscribe("post_sync_phase_changed", (payload) => {
            const phase = String(payload?.phase ?? "");
            const active = Boolean(payload?.active ?? false);
            if (active || !phase)
                return;
            this._pendingPhases.delete(phase);
            if (this._pendingPhases.size === 0) {
                this._update({ isSyncing: false, isCancelling: false });
                this._stopPolling();
                if (this._pendingRestart && this._observedActiveSync) {
                    this._pendingRestart = false;
                    try {
                        DFL.showModal(SP_JSX.jsx(SteamRestartModal, { reason: "sync", closeModal: () => { } }));
                    }
                    catch (e) {
                        console.error("[SyncStore] showModal(SteamRestartModal) failed", e);
                    }
                }
                else if (this._pendingRestart) {
                    // Replay path: clear flag so a later sync can re-arm.
                    this._pendingRestart = false;
                }
            }
        }));
        this._unsubs.push(EventBusClient.subscribe("sync_failed", () => {
            this._pendingPhases.clear();
            this._update({ isSyncing: false, isCancelling: false });
            this._stopPolling();
        }));
        this._unsubs.push(EventBusClient.subscribe("sync_cancelled", () => {
            this._pendingPhases.clear();
            this._update({
                isSyncing: false,
                isCancelling: false,
                progress: null,
            });
            this._stopPolling();
        }));
        this._unsubs.push(EventBusClient.subscribe("shortcut_reconcile_complete", (payload) => {
            const added = Number(payload?.added ?? 0);
            const removed = Number(payload?.removed ?? 0);
            if (added > 0 || removed > 0) {
                this._pendingRestart = true;
            }
        }));
    }
    /** Stop all subscriptions and polling. */
    stop() {
        this._stopPolling();
        for (const unsub of this._unsubs)
            unsub();
        this._unsubs = [];
    }
    /** Notify the store that a sync was started by the user
     *  (called from SyncContext before the RPC). */
    notifySyncStarted() {
        this._observedActiveSync = true;
        this._update({
            isSyncing: true,
            isCancelling: false,
            progress: null,
        });
        EventBusClient.bumpToFast();
        this._startPolling();
        void this._pollOnce();
    }
    /** Notify the store that a cancel was requested. */
    notifyCancelRequested() {
        this._update({ isCancelling: true, progress: null });
    }
    // ── Internals ─────────────────────────────────────────
    async _pollOnce() {
        try {
            const raw = await call(rpcRoutes.getSyncProgress);
            const data = unwrapRpcEnvelope(raw, { route: rpcRoutes.getSyncProgress, throwing: false });
            if (!data)
                return;
            const partial = { progress: data };
            if (typeof data.syncing === "boolean") {
                partial.isSyncing = data.syncing;
                if (data.syncing) {
                    // A sync is in flight (e.g. restored at boot, or a
                    // background/scheduled run). Keep the 500ms loop alive so
                    // progress refreshes — we no longer rely on a replayed
                    // ``sync_started`` to start polling (those are primed past
                    // on reload). ``get_status`` reports syncing through the
                    // post-sync phases too, so this also self-clears the bar
                    // (status → "complete") when the run actually finishes.
                    this._startPolling();
                }
                else {
                    partial.isCancelling = false;
                    this._stopPolling();
                }
            }
            if (typeof data.cooldown_ms === "number" && data.cooldown_ms >= 0) {
                setSyncCooldownMs(data.cooldown_ms);
            }
            this._update(partial);
        }
        catch (e) {
            console.warn("[SyncStore] poll failed", e);
        }
    }
    _startPolling() {
        if (this._pollTimer)
            return;
        this._pollTimer = setInterval(() => void this._pollOnce(), PROGRESS_POLL_MS);
    }
    _stopPolling() {
        if (this._pollTimer) {
            clearInterval(this._pollTimer);
            this._pollTimer = null;
        }
    }
    _update(partial) {
        this._snapshot = { ...this._snapshot, ...partial };
        this._emit();
    }
    _emit() {
        for (const listener of this._listeners)
            listener();
    }
}
/** Singleton — started at boot from `definePlugin`. */
const syncStore = new SyncStoreImpl();

/**
 * Tab definitions and per-tab collection container for the custom
 * Unifideck library tabs. Ported from staging:src/tabs/TabContainer.ts.
 *
 * `UNIFIDECK_TABS` declares the 10 tabs spliced into Steam's library.
 * `UnifideckTabContainer` wraps Steam's collection shape so each tab
 * is treated as a first-class collection (count, sort, filter).
 * `tabManager` is the singleton that the library-patch hook reads to
 * know which tabs to inject.
 */

const t = (key) => instance.t(key);
function getUnifideckTabs() {
    return [
        {
            id: "unifideck-deck",
            title: t("deckTabs.greatOnDeck"),
            position: 0,
            filters: [{ type: "deckCompat", params: {} }],
        },
        {
            id: "unifideck-all",
            title: t("deckTabs.allGames"),
            position: 1,
            filters: [{ type: "all", params: {} }],
        },
        {
            id: "unifideck-installed",
            title: t("deckTabs.installed"),
            position: 2,
            filters: [{ type: "installed", params: { installed: true } }],
        },
        {
            id: "unifideck-steam",
            title: t("deckTabs.steam"),
            position: 3,
            filters: [{ type: "store", params: { store: "steam" } }],
        },
        {
            id: "unifideck-epic",
            title: t("deckTabs.epic"),
            position: 4,
            filters: [{ type: "store", params: { store: "epic" } }],
        },
        {
            id: "unifideck-gog",
            title: t("deckTabs.gog"),
            position: 5,
            filters: [{ type: "store", params: { store: "gog" } }],
        },
        {
            id: "unifideck-amazon",
            title: t("deckTabs.amazon"),
            position: 6,
            filters: [{ type: "store", params: { store: "amazon" } }],
        },
        {
            id: "unifideck-ubisoft",
            title: t("deckTabs.ubisoft"),
            position: 7,
            filters: [{ type: "store", params: { store: "ubisoft" } }],
        },
        {
            id: "unifideck-microsoft",
            title: t("deckTabs.microsoft"),
            position: 8,
            filters: [{ type: "store", params: { store: "microsoft" } }],
        },
        {
            id: "unifideck-nonsteam",
            title: t("deckTabs.nonSteam"),
            position: 9,
            filters: [{ type: "nonSteam", params: {} }],
        },
    ];
}
const DEFAULT_TABS_TO_HIDE = [
    "GreatOnDeck",
    "AllGames",
    "Installed",
    "DesktopApps",
];
function isTabMasterInstalled() {
    try {
        const plugins = window.DeckyPluginLoader?.plugins ?? [];
        return plugins.some((p) => p.name === "TabMaster" || p.name === "Tab Master");
    }
    catch {
        return false;
    }
}
function getHiddenDefaultTabs() {
    if (isTabMasterInstalled()) {
        console.log("[Unifideck] TabMaster detected — keeping default tabs visible");
        return [];
    }
    return DEFAULT_TABS_TO_HIDE;
}
// Bounded re-render retry for the collectionStore-not-hydrated race.
// When ``buildCollection`` runs before Steam's ``type-games``
// collection exists, every tab renders 0 and stays there until an
// unrelated re-render (UD-071). We ping the tab manager a few times
// with backoff so the strip re-renders — and re-runs
// ``buildCollection`` — once the store hydrates. Module-level (not
// per-container) because one hydration fixes every tab, so a single
// shared budget avoids N parallel timers.
const _HYDRATION_RETRY_MAX = 8;
const _HYDRATION_RETRY_BASE_MS = 400;
let hydrationRetryCount = 0;
let hydrationRetryTimer = null;
function scheduleHydrationRetry() {
    if (hydrationRetryTimer !== null)
        return; // one in flight already
    if (hydrationRetryCount >= _HYDRATION_RETRY_MAX)
        return;
    hydrationRetryCount += 1;
    const delay = _HYDRATION_RETRY_BASE_MS * hydrationRetryCount;
    hydrationRetryTimer = setTimeout(() => {
        hydrationRetryTimer = null;
        if (tabManager.isInitialized())
            tabManager.rebuildTabs();
    }, delay);
}
function resetHydrationRetry() {
    hydrationRetryCount = 0;
    if (hydrationRetryTimer !== null) {
        clearTimeout(hydrationRetryTimer);
        hydrationRetryTimer = null;
    }
}
class UnifideckTabContainer {
    constructor(tab) {
        this.id = tab.id;
        this.title = tab.title;
        this.position = tab.position;
        this.filters = tab.filters;
        this.collection = this.makeEmptyCollection();
        // Don't call buildCollection here — at plugin-init time the
        // collectionStore is half-hydrated and its MobX-computed
        // getters throw. ``getActualTab`` calls ``buildCollection``
        // at render time when the store is fully ready, which is
        // when it actually matters.
    }
    makeEmptyCollection() {
        return {
            AsDeletableCollection: () => null,
            AsDragDropCollection: () => null,
            AsEditableCollection: () => null,
            // A throw here propagates through Steam's tab renderer and
            // error-boundaries the ENTIRE library, so this must never
            // trust the filter's shape (Steam Beta 2026-07 moved it and
            // handed us a collection object instead). Unfiltered count is
            // the graceful fallback — visibleApps is already tab-filtered.
            GetAppCountWithToolsFilter: (appFilter) => {
                if (typeof appFilter?.Matches !== "function") {
                    return this.collection.visibleApps.length;
                }
                try {
                    return this.collection.visibleApps.filter((a) => appFilter.Matches(a))
                        .length;
                }
                catch {
                    return this.collection.visibleApps.length;
                }
            },
            bAllowsDragAndDrop: false,
            bIsDeletable: false,
            bIsDynamic: false,
            bIsEditable: false,
            displayName: this.title,
            id: this.id,
            allApps: [],
            visibleApps: [],
            apps: new Map(),
        };
    }
    buildCollection() {
        try {
            const cs = window.collectionStore;
            // Prefer the raw appTypeCollectionMap (TabMaster's path) —
            // direct Map access doesn't go through MobX-computed
            // getters that may throw on half-hydrated stores. Fall back
            // to GetCollection if the raw map is unavailable.
            const all = cs?.appTypeCollectionMap?.get("type-games") ??
                cs?.GetCollection("type-games");
            if (!all) {
                // Steam's collectionStore isn't hydrated yet. Leaving the
                // collection empty here latches every tab to 0 until the
                // next unrelated re-render (UD-071 "works, then 0"). Schedule
                // a bounded re-render so tabs backfill once the store is ready.
                scheduleHydrationRetry();
                return;
            }
            // Store is hydrated — clear any pending hydration retry so a
            // later genuinely-empty library doesn't inherit stale budget.
            resetHydrationRetry();
            const filtered = (all.allApps ?? []).filter((app) => runFilters(this.filters, app));
            this.collection.allApps = filtered;
            this.collection.visibleApps = [...filtered];
            const map = new Map();
            for (const a of filtered)
                map.set(a.appid, a);
            this.collection.apps = map;
        }
        catch (e) {
            console.error("[Unifideck] buildCollection failed", e);
        }
    }
    getActualTab(TabAppGrid, TabContext, sortingProps, collectionAppFilter, templateFooter) {
        this.buildCollection();
        const inner = SP_REACT.createElement(TabAppGrid, {
            collection: this.collection,
            ...sortingProps,
        });
        const content = TabContext
            ? SP_REACT.createElement(TabContext.Provider, { value: { label: this.title } }, inner)
            : inner;
        return {
            title: this.title,
            id: this.id,
            // Inherit the AllGames template's footer (keybinding hints,
            // menu callbacks). Steam's gamepad tab renderer expects
            // these fields populated; an empty ``{}`` makes tabs render
            // as no-op entries that are skipped in the nav strip.
            footer: { ...(templateFooter ?? {}) },
            content,
            // Steam invokes this on every tab-strip render; any throw
            // escapes into Steam's library error boundary, so the count
            // is computed defensively no matter what shape Steam hands us.
            renderTabAddon: () => {
                let count;
                try {
                    count =
                        this.collection.GetAppCountWithToolsFilter(collectionAppFilter);
                }
                catch {
                    count = this.collection.visibleApps.length;
                }
                return SP_REACT.createElement("span", { className: DFL.gamepadTabbedPageClasses?.TabCount ?? "" }, count);
            },
        };
    }
}
class TabManager {
    constructor() {
        this.tabs = [];
        this.initialized = false;
        this.storeCounts = {
            epic: 0,
            gog: 0,
            amazon: 0,
            ubisoft: 0,
            microsoft: 0,
        };
        this.version = 0;
        this.listeners = [];
    }
    getVersion() {
        return this.version;
    }
    onTabsChanged(listener) {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter((l) => l !== listener);
        };
    }
    notifyListeners() {
        this.version++;
        this.listeners.forEach((l) => {
            try {
                l();
            }
            catch (e) {
                console.error("[TabManager] Listener failed:", e);
            }
        });
    }
    initialize() {
        if (this.initialized)
            return;
        this.tabs = getUnifideckTabs().map((tab) => new UnifideckTabContainer(tab));
        this.initialized = true;
    }
    getTabs() {
        return this.tabs.filter((t) => this.shouldShowTab(t.id));
    }
    setStoreCounts(counts) {
        this.storeCounts = { ...this.storeCounts, ...counts };
    }
    // A per-store tab is shown only when that store has at least one
    // game. Connection/login state is deliberately ignored — an empty
    // store (even one the user is logged into) hides its tab until
    // games sync in, and reappears once they do.
    shouldShowTab(id) {
        const m = {
            "unifideck-epic": "epic",
            "unifideck-gog": "gog",
            "unifideck-amazon": "amazon",
            "unifideck-ubisoft": "ubisoft",
            "unifideck-microsoft": "microsoft",
        };
        const store = m[id];
        if (!store)
            return true;
        return this.storeCounts[store] > 0;
    }
    isInitialized() {
        return this.initialized;
    }
    rebuildTabs() {
        this.tabs = getUnifideckTabs().map((tab) => new UnifideckTabContainer(tab));
        this.notifyListeners();
    }
}
const tabManager = new TabManager();
// Wire the cache loader → tab manager so per-store counts drive
// ``shouldShowTab`` automatically. The sink is module-level state
// on ``library-filters`` so a single registration covers every
// future ``loadUnifideckCache`` invocation.
setStoreCountSink((counts) => {
    tabManager.setStoreCounts(counts);
    if (tabManager.isInitialized())
        tabManager.rebuildTabs();
});

/**
 * CollectionManager — auto-generates `[Unifideck] *` Steam Collections
 * mirroring the UNIFIDECK_TABS list.
 *
 * Ported from `staging:src/spoofing/CollectionManager.ts`. Wires into
 * Steam's `window.collectionStore` and `window.appStore` — the only
 * two Steam globals not abstracted by `SteamBridge` because the
 * collection APIs are too coupled to Steam's React tree to project.
 */
const COLLECTION_PREFIX = "[Unifideck] ";
// Non-Steam shortcuts (ours + the user's) carry this app_type; exclude
// them so we never feed Ubisoft titles back into the Steam-owned filter.
const NON_STEAM_SHORTCUT_APP_TYPE = 1073741824;
/**
 * Master opt-in for `[Unifideck]` Steam Collections. Default OFF — except
 * when TabMaster is installed, where it defaults ON (TabMaster suppresses
 * our own injected library tabs, so collections are the only organization
 * we can offer those users).
 *
 * Steam keeps ALL user collections in one CEF-localStorage blob and
 * auto-syncs that whole namespace to Steam Cloud, so any collection we
 * create propagates to the user's other devices — even ones without the
 * plugin — and into Desktop mode. There is no per-collection "local only"
 * flag. So collections are opt-in: most users get the in-React-tree
 * library tabs (which sync nowhere) and never touch the cloud.
 *
 * Stored as "1"/"0" so an explicitly-chosen value is distinguishable
 * from "never set" (the grandfather migration below relies on that).
 * `setCollectionsEnabled` broadcasts `COLLECTIONS_ENABLED_EVENT` so the
 * QAM toggle and the running manager stay in sync without a restart.
 */
const COLLECTIONS_ENABLED_KEY = "unifideck:collections.enabled";
const COLLECTIONS_ENABLED_EVENT = "unifideck:collections-enabled-change";
/**
 * Grandfather marker. Before collections became opt-in they were created
 * for everyone, so on the first run of this build we keep them for users
 * who already have them (see `migrateGrandfatherExisting`). Set once so
 * the detection never re-runs.
 */
const COLLECTIONS_MIGRATED_KEY = "unifideck:collections.migrated";
/**
 * One-time-cleanup marker for the OFF state. When collections are off we
 * delete any leftover `[Unifideck]` collections exactly ONCE, then leave
 * the blob alone. Cleaning on every boot would thrash against a *different*
 * device that has them enabled and keeps re-creating them — collections
 * are account-global, so an OFF device can't truly hide what an ON device
 * insists on creating.
 */
const COLLECTIONS_CLEANED_KEY = "unifideck:collections.cleaned";
function isCollectionsEnabled() {
    try {
        const v = window.localStorage.getItem(COLLECTIONS_ENABLED_KEY);
        if (v === "1")
            return true;
        if (v === "0")
            return false;
    }
    catch {
        /* fall through to the default */
    }
    // No explicit choice yet — default ON when TabMaster is installed (our
    // own tabs are suppressed under it), OFF for everyone else. TabMaster
    // detection is synchronous (reads DeckyPluginLoader), so this is safe to
    // call before the collection store hydrates.
    return isTabMasterInstalled();
}
/** True once the user (or the grandfather migration) has chosen a value. */
function isEnabledExplicit() {
    try {
        return window.localStorage.getItem(COLLECTIONS_ENABLED_KEY) != null;
    }
    catch {
        return false;
    }
}
function setCollectionsEnabled(on) {
    try {
        window.localStorage.setItem(COLLECTIONS_ENABLED_KEY, on ? "1" : "0");
    }
    catch {
        /* localStorage unavailable — worst case is pre-fix behavior */
    }
    try {
        window.dispatchEvent(new CustomEvent(COLLECTIONS_ENABLED_EVENT, { detail: on }));
    }
    catch {
        /* CEF can deny CustomEvent in rare half-loaded states */
    }
}
function isCleaned() {
    try {
        return window.localStorage.getItem(COLLECTIONS_CLEANED_KEY) === "1";
    }
    catch {
        return false;
    }
}
function setCleaned(on) {
    try {
        if (on)
            window.localStorage.setItem(COLLECTIONS_CLEANED_KEY, "1");
        else
            window.localStorage.removeItem(COLLECTIONS_CLEANED_KEY);
    }
    catch {
        /* ignore */
    }
}
function hasMigrated() {
    try {
        return window.localStorage.getItem(COLLECTIONS_MIGRATED_KEY) === "1";
    }
    catch {
        return false;
    }
}
function setMigrated() {
    try {
        window.localStorage.setItem(COLLECTIONS_MIGRATED_KEY, "1");
    }
    catch {
        /* ignore */
    }
}
/**
 * One-time grandfather migration. Collections used to be created for
 * everyone, so users already relying on them should keep them. On the
 * first run of the opt-in build, if the user has never chosen the setting
 * AND `[Unifideck]` collections already exist in the store, default this
 * account to ENABLED so we don't pull away tabs they use. Fresh installs
 * (no such collections) stay OFF. Writes localStorage directly — NOT via
 * `setCollectionsEnabled` — so it doesn't fire the toggle event (this is a
 * silent default, not a user action). Caller must ensure the store is
 * hydrated first so the collection scan is meaningful.
 */
function migrateGrandfatherExisting(cs) {
    if (hasMigrated())
        return;
    if (!isEnabledExplicit() && snapshotUnifideckCollections(cs).length > 0) {
        try {
            window.localStorage.setItem(COLLECTIONS_ENABLED_KEY, "1");
        }
        catch {
            /* ignore — falls back to OFF, recoverable via the QAM toggle */
        }
    }
    setMigrated();
}
function getCollectionStore() {
    return (window
        .collectionStore ?? null);
}
function getAppStore$2() {
    return window.appStore ?? null;
}
function tabName(tab) {
    return `${COLLECTION_PREFIX}${tab.title}`;
}
function validCollectionNames() {
    return new Set(getUnifideckTabs().map(tabName));
}
async function deleteCollection(c) {
    try {
        await c.Delete();
    }
    catch (e) {
        console.error(`[Unifideck Collections] delete ${c.displayName} failed`, e);
    }
}
/**
 * Snapshot of every `[Unifideck]` collection. `Delete()` mutates the
 * underlying MobX Map, so iterating `userCollections.values()` live
 * while deleting skips entries — always work from a snapshot.
 */
function snapshotUnifideckCollections(cs) {
    let collections;
    try {
        collections = cs.userCollections;
    }
    catch {
        return [];
    }
    if (!collections || typeof collections.values !== "function")
        return [];
    return Array.from(collections.values()).filter((c) => c?.displayName?.startsWith(COLLECTION_PREFIX));
}
async function cleanupStaleCollections() {
    const cs = getCollectionStore();
    if (!cs)
        return;
    const valid = validCollectionNames();
    for (const c of snapshotUnifideckCollections(cs)) {
        if (!valid.has(c.displayName)) {
            await deleteCollection(c);
        }
    }
}
async function getOrCreateCollection(tag) {
    const cs = getCollectionStore();
    if (!cs)
        return null;
    const id = cs.GetCollectionIDByUserTag(tag);
    if (typeof id === "string") {
        const existing = cs.GetCollection(id);
        if (existing)
            return existing;
    }
    const created = cs.NewUnsavedCollection(tag, undefined, []);
    if (!created)
        return null;
    await created.Save();
    return created;
}
async function clearCollection(c) {
    const apps = c.allApps ?? [];
    if (apps.length === 0)
        return;
    c.AsDragDropCollection().RemoveApps(apps);
    await c.Save();
}
async function syncTab(tab, allApps) {
    const matches = allApps.filter((a) => a.appid > 0 && runFilters(tab.filters, a));
    if (matches.length === 0) {
        // Nothing to show — don't create an empty `[Unifideck]` shell, and
        // drop any leftover one from a previous sync.
        const cs = getCollectionStore();
        if (!cs)
            return false;
        const id = cs.GetCollectionIDByUserTag(tabName(tab));
        if (typeof id === "string") {
            const existing = cs.GetCollection(id);
            if (existing)
                await deleteCollection(existing);
        }
        return true;
    }
    const c = await getOrCreateCollection(tabName(tab));
    if (!c)
        return false;
    const appStore = getAppStore$2();
    if (!appStore)
        return false;
    await clearCollection(c);
    const overviews = [];
    for (const a of matches) {
        try {
            const o = appStore.GetAppOverviewByAppID(a.appid);
            if (o)
                overviews.push(o);
        }
        catch {
            /* skip */
        }
    }
    if (overviews.length > 0) {
        c.AsDragDropCollection().AddApps(overviews);
        await c.Save();
    }
    return true;
}
/** Sync every `[Unifideck]` collection to current tab filters. */
async function syncUnifideckCollections() {
    if (!isCollectionsEnabled())
        return;
    if (!isCollectionsAvailable())
        return;
    await cleanupStaleCollections();
    const cs = getCollectionStore();
    if (!cs)
        return;
    let allApps = [];
    try {
        const games = cs.GetCollection("type-games");
        allApps = (games?.allApps ?? []);
    }
    catch {
        return;
    }
    if (allApps.length === 0)
        return;
    await Promise.allSettled(getUnifideckTabs().map((t) => syncTab(t, allApps)));
}
/**
 * Display names of every Steam game the user owns — installed or not —
 * from Steam's "type-games" collection. Non-Steam shortcuts are excluded
 * (see {@link NON_STEAM_SHORTCUT_APP_TYPE}). `appmanifest` only knows
 * installed games, so this is the only way the backend learns about
 * owned-but-not-installed Steam games.
 */
function collectSteamOwnedGameTitles() {
    const cs = getCollectionStore();
    if (!cs)
        return [];
    let allApps = [];
    try {
        const games = cs.GetCollection("type-games");
        allApps = (games?.allApps ?? []);
    }
    catch {
        return [];
    }
    const titles = new Set();
    for (const a of allApps) {
        if (!a || a.appid <= 0 || a.app_type === NON_STEAM_SHORTCUT_APP_TYPE) {
            continue;
        }
        const name = a.display_name?.trim();
        if (name)
            titles.add(name);
    }
    return Array.from(titles);
}
/**
 * Delete every `[Unifideck]` collection. Verifies the store actually
 * dropped them (deletes persist asynchronously and can race a Steam
 * restart), retrying leftovers a few times before giving up. Re-creation
 * is governed by the enabled flag / cleaned marker, not by this function.
 */
async function deleteAllUnifideckCollections() {
    const cs = getCollectionStore();
    if (!cs)
        return;
    // Tag-based pass first — deterministic lookup for the current locale;
    // the prefix scan below also catches collections created under a
    // different UI language.
    for (const tab of getUnifideckTabs()) {
        try {
            const id = cs.GetCollectionIDByUserTag(tabName(tab));
            if (typeof id === "string") {
                const c = cs.GetCollection(id);
                if (c)
                    await deleteCollection(c);
            }
        }
        catch {
            /* skip */
        }
    }
    for (let attempt = 0; attempt < 3; attempt++) {
        const targets = snapshotUnifideckCollections(cs);
        if (targets.length === 0)
            return;
        for (const c of targets)
            await deleteCollection(c);
        await new Promise((r) => setTimeout(r, 250));
    }
    const survivors = snapshotUnifideckCollections(cs);
    if (survivors.length > 0) {
        console.error("[Unifideck Collections] collections survived deletion:", survivors.map((c) => c.displayName));
    }
}
function isCollectionsAvailable() {
    const s = getCollectionStore();
    if (!s ||
        typeof s.GetCollectionIDByUserTag !== "function" ||
        typeof s.NewUnsavedCollection !== "function")
        return false;
    try {
        // ``GetCollection("type-games")`` is a synchronous lookup
        // by tag; it accesses the underlying store directly without
        // going through the ``userCollections`` MobX-computed getter
        // that throws when the store is half-hydrated. Once
        // ``type-games`` is resolvable with a real ``allApps`` array,
        // the whole collection graph is safe to traverse.
        const games = s.GetCollection("type-games");
        return Boolean(games && Array.isArray(games.allApps));
    }
    catch {
        return false;
    }
}
async function waitForCollections(timeoutMs = 30000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        if (isCollectionsAvailable())
            return true;
        await new Promise((r) => setTimeout(r, 500));
    }
    return false;
}
/**
 * Collection manager. Collections are opt-in ({@link isCollectionsEnabled}):
 *
 * - ON  → subscribe to `unifideck-sync-completed` and rebuild the
 *   `[Unifideck]` collections after every library sync (original behavior).
 * - OFF → delete any leftover `[Unifideck]` collections exactly once (see
 *   {@link isCleaned}) and otherwise stay out of the way.
 *
 * On first run it grandfathers users who already have collections to ON
 * (see {@link migrateGrandfatherExisting}). Reacts live to
 * `COLLECTIONS_ENABLED_EVENT` (the QAM toggle) so flipping the setting
 * takes effect without a Steam restart — turning it OFF deletes the
 * collections immediately. `remove()` detaches every listener.
 */
function startCollectionManager() {
    let syncAttached = false;
    const onSync = () => {
        void syncUnifideckCollections().catch((e) => console.error("[Unifideck Collections] resync failed", e));
    };
    const attachSync = () => {
        if (syncAttached)
            return;
        window.addEventListener("unifideck-sync-completed", onSync);
        syncAttached = true;
    };
    const detachSync = () => {
        if (!syncAttached)
            return;
        window.removeEventListener("unifideck-sync-completed", onSync);
        syncAttached = false;
    };
    // Build/maintain collections for the ON state. `syncUnifideckCollections`
    // self-guards on the enabled flag and store readiness, so this is safe to
    // call eagerly.
    const enabledSync = () => void syncUnifideckCollections().catch((e) => console.error("[Unifideck Collections] sync failed", e));
    // Drop leftover collections for the OFF state, once.
    const cleanupOnce = () => {
        if (isCleaned())
            return;
        void deleteAllUnifideckCollections()
            .then(() => setCleaned(true))
            .catch((e) => console.error("[Unifideck Collections] cleanup failed", e));
    };
    // React to the QAM toggle without a restart.
    const onEnabledChange = (e) => {
        const enabled = Boolean(e.detail);
        if (enabled) {
            // Opted back in — let a future opt-out clean up again, then rebuild.
            setCleaned(false);
            attachSync();
            enabledSync();
        }
        else {
            // Opted out — collections are deleted immediately.
            detachSync();
            void deleteAllUnifideckCollections()
                .then(() => setCleaned(true))
                .catch((err) => console.error("[Unifideck Collections] disable cleanup failed", err));
        }
    };
    window.addEventListener(COLLECTIONS_ENABLED_EVENT, onEnabledChange);
    // Boot: wait for the store to hydrate, grandfather existing users, then
    // apply the resolved ON/OFF state. We can only decide after migration
    // (which needs the store), so the sync listener is attached here rather
    // than synchronously — the initial sync below covers anything missed.
    void waitForCollections()
        .then((ready) => {
        const cs = ready ? getCollectionStore() : null;
        if (cs)
            migrateGrandfatherExisting(cs);
        if (isCollectionsEnabled()) {
            attachSync();
            if (ready)
                enabledSync();
            else
                console.warn("[Unifideck Collections] store never became ready — skipping initial sync");
        }
        else if (ready) {
            cleanupOnce();
        }
    })
        .catch((e) => console.error("[Unifideck Collections] startup failed", e));
    return {
        resync: syncUnifideckCollections,
        remove: () => {
            detachSync();
            window.removeEventListener(COLLECTIONS_ENABLED_EVENT, onEnabledChange);
        },
    };
}

/**
 * Push the user's full owned Steam library to the backend.
 *
 * Kept separate from `collection-manager.ts` (which must stay free of
 * the `@decky/api` runtime so its unit tests can import it under vitest)
 * — this module is the thin RPC edge that the pure
 * `collectSteamOwnedGameTitles` collector feeds.
 *
 * The backend's Ubisoft Steam-linked filter unions these titles in to
 * hide Ubisoft games the user owns on Steam but hasn't installed (those
 * never appear in `appmanifest`). See
 * `py_modules/unifideck/stores/ubisoft/library/steam_filter.py`.
 */
/**
 * Best effort — on failure the backend filter just degrades to the
 * installed-games-only scan.
 */
async function uploadSteamOwnedTitles() {
    try {
        const titles = collectSteamOwnedGameTitles();
        if (titles.length === 0)
            return;
        await call(rpcRoutes.updateSteamOwnedTitles, titles);
    }
    catch (e) {
        console.warn("[Unifideck] uploadSteamOwnedTitles failed", e);
    }
}

/**
 * Read the live logged-in Steam user and push it to the backend.
 *
 * The frontend runs *inside* the Steam client, so `window.App.m_CurrentUser`
 * is the ONLY 100%-correct source of the active account. The backend
 * otherwise falls back to disk heuristics that can pick the wrong
 * `userdata/<id>` on multi-account decks — writing `shortcuts.vdf` where the
 * running client never reads it ("synced N games, Steam shows 0"). Pushing the
 * live id here lets the backend resolve the correct per-user paths.
 *
 * The pure `steam64ToAccountId` conversion is exported separately (no
 * `@decky/api` dependency) so it is unit-testable under vitest, mirroring how
 * `collection-manager.ts` stays runtime-free for its tests.
 */
/**
 * Convert a SteamID64 string to the 32-bit account id Steam uses as the
 * `userdata/<id>` folder name (`steamID64 & 0xFFFFFFFF`).
 *
 * MUST use BigInt: a SteamID64 exceeds 2^53, so `Number(s) & 0xFFFFFFFF`
 * loses precision and yields a WRONG id (e.g. 225630048 instead of
 * 225630054). Returns `null` for malformed input.
 */
function steam64ToAccountId(steam64) {
    if (!steam64)
        return null;
    const trimmed = String(steam64).trim();
    if (!/^\d{6,}$/.test(trimmed))
        return null;
    try {
        return String(BigInt(trimmed) & 0xffffffffn);
    }
    catch {
        return null;
    }
}
/**
 * Read the live logged-in account id from Steam's globals.
 * Tries `window.App.m_CurrentUser.strSteamID` first (confirmed on-device),
 * then `loginStore` as a fallback. Returns `null` if none resolve.
 */
function readLiveAccountId() {
    // Use ``globalThis`` (not a bare ``window``) so this never throws a
    // ReferenceError in a non-window context — it just resolves to null.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const w = globalThis;
    const steam64 = w?.App?.m_CurrentUser?.strSteamID ?? w?.loginStore?.m_strSteamID;
    return steam64ToAccountId(steam64);
}
/**
 * Best effort — on failure the backend just falls back to its disk
 * heuristics, so a missing global or a rejected `call` is a silent no-op.
 */
async function uploadActiveSteamUser() {
    try {
        const accountId = readLiveAccountId();
        if (!accountId || accountId === "0")
            return;
        await call(rpcRoutes.setActiveSteamUser, accountId);
    }
    catch (e) {
        console.warn("[Unifideck] uploadActiveSteamUser failed", e);
    }
}

const Ctx$1 = SP_REACT.createContext(null);
/**
 * Provider that exposes sync state and actions. State comes from
 * the boot-time `syncStore` singleton; actions are thin wrappers
 * around RPC mutations that also notify the store.
 */
const SyncProvider = ({ children }) => {
    const { progress, isSyncing, isCancelling } = SP_REACT.useSyncExternalStore(syncStore.subscribe, syncStore.getSnapshot);
    const startMut = useRPCMutation(rpcRoutes.syncLibraries);
    const forceMut = useRPCMutation(rpcRoutes.forceSyncLibraries);
    const cancelMut = useRPCMutation(rpcRoutes.cancelSync);
    const startSync = SP_REACT.useCallback(async () => {
        if (isSyncing)
            return;
        syncStore.notifySyncStarted();
        // Confirm the live active Steam user + refresh the owned-library snapshot
        // before the backend fetch, so shortcuts land in the right userdata dir
        // and Steam-linked Ubisoft games are hidden this run.
        await uploadActiveSteamUser();
        await uploadSteamOwnedTitles();
        void startMut
            .mutate()
            .catch((e) => console.warn("[SyncContext] startSync RPC failed", e));
    }, [isSyncing, startMut]);
    const forceSync = SP_REACT.useCallback(async (resyncArtwork) => {
        syncStore.notifySyncStarted();
        await uploadActiveSteamUser();
        await uploadSteamOwnedTitles();
        void forceMut
            .mutate(resyncArtwork)
            .catch((e) => console.warn("[SyncContext] forceSync RPC failed", e));
    }, [forceMut]);
    const cancelSync = SP_REACT.useCallback(async () => {
        if (!isSyncing || isCancelling)
            return;
        syncStore.notifyCancelRequested();
        await cancelMut.mutate();
    }, [isSyncing, isCancelling, cancelMut]);
    const value = {
        progress,
        isSyncing,
        isCancelling,
        startSync,
        forceSync,
        cancelSync,
    };
    return SP_JSX.jsx(Ctx$1.Provider, { value: value, children: children });
};
/**
 * Access the SyncContext value. Throws if used
 * outside `<SyncProvider>` — a tree-wiring bug.
 *
 * @throws Error when the provider is missing.
 */
function useSync() {
    const v = SP_REACT.useContext(Ctx$1);
    if (!v)
        throw new Error("useSync called outside <SyncProvider>");
    return v;
}

/**
 * useGameInfo — per-appId info fetch with TTL cache.
 *
 * Replaces the global `gameInfoCache` Map that lived in the
 * old `index.tsx` (and was passed via setter callbacks all
 * over the place). Each component that needs game info just
 * calls `useGameInfo(appId)` and gets reactive {data, loading,
 * error, refresh}. The hook shares a module-level cache so
 * concurrent consumers of the same appId don't re-fetch.
 *
 * Cache invariants :
 *  - TTL = 5000ms (matches old behaviour)
 *  - Same appId across components = single fetch in flight
 *  - `refresh()` always re-fetches (bypasses cache)
 */

/**
 * Adapt the raw ``get_game_info`` RPC response into our
 * frontend ``Game`` shape.
 *
 * Backend's :class:`Game` dataclass uses ``installed`` /
 * ``store_game_id`` / ``exe_path``; the frontend Game interface
 * (older shape, predates the unified-types refactor) expects
 * ``is_installed`` / ``id`` / ``executable``. Without this
 * adapter, every consumer of ``useGameInfo`` sees
 * ``game.is_installed === undefined`` (falsy → "not installed")
 * and ``game.id === undefined`` (so download-queue matching by
 * ``game.id === download.game_id`` always misses), which is why
 * the Play section stays on Install even mid-download.
 */
function adaptGame(raw) {
    if (!raw || typeof raw !== "object")
        return null;
    const r = raw;
    const storeGameId = String(r.store_game_id ?? r.id ?? "");
    if (!storeGameId)
        return null;
    return {
        id: storeGameId,
        store_game_id: storeGameId,
        title: String(r.title ?? ""),
        store: (r.store ?? "unknown"),
        is_installed: Boolean(r.installed ?? r.is_installed),
        install_path: typeof r.install_path === "string" ? r.install_path : undefined,
        executable: typeof r.exe_path === "string"
            ? r.exe_path
            : typeof r.executable === "string"
                ? r.executable
                : undefined,
        app_id: typeof r.app_id === "number" ? r.app_id : undefined,
        size_bytes: typeof r.size_bytes === "number" ? r.size_bytes : undefined,
        cover_image: typeof r.cover_image === "string" ? r.cover_image : undefined,
        // Backend serialises store tags as ``tags`` (e.g. ``["xcloud"]``
        // for Xbox Cloud games); expose them as ``store_tags`` so the
        // play-section logic can branch on cloud-streaming titles.
        store_tags: Array.isArray(r.store_tags)
            ? r.store_tags
            : Array.isArray(r.tags)
                ? r.tags
                : undefined,
    };
}
const CACHE_TTL$1 = 5000;
const cache$2 = new Map();
// Mounted-hook subscribers, keyed by appId. `invalidateGameInfo`
// notifies these so a live `useGameInfo` refetches immediately
// (e.g. when a download completes and `is_installed` flips) —
// clearing the module cache alone doesn't re-run a mounted hook,
// which is why the Play section used to stay on "Install" until
// the user reopened the page.
const subscribers = new Map();
function subscribeGameInfo(appId, fn) {
    let set = subscribers.get(appId);
    if (!set) {
        set = new Set();
        subscribers.set(appId, set);
    }
    set.add(fn);
    return () => {
        set?.delete(fn);
        if (set && set.size === 0)
            subscribers.delete(appId);
    };
}
function notifyGameInfo(appId) {
    subscribers.get(appId)?.forEach((fn) => fn());
}
/**
 * Hook that aggregates all metadata Unifideck has on
 * a given game : description, scores, artwork,
 * playtime stats. Uses `useRPCQuery` under the hood
 * with a sensible cache TTL so opening the info panel
 * is instant on revisits.
 *
 * @param appId — Steam shortcut app-id.
 * @returns aggregated info + loading/error flags.
 */
function useGameInfo(appId) {
    // Backend's `get_game_metadata(store, game_id)` requires a
    // store/game-id pair we don't have at the appId boundary.
    // `get_game_info(app_id)` is the right route for "look up
    // by Steam shortcut appid".
    // We receive the raw backend dict (snake_case, ``installed``
    // not ``is_installed``, etc.) and adapt it via ``adaptGame``
    // below; declaring the RPC return as ``unknown`` keeps the
    // type system honest about the wire shape.
    const fetchRaw = useRPC(rpcRoutes.getGameInfo);
    const fetch = SP_REACT.useCallback(async (id) => adaptGame(await fetchRaw(id)), [fetchRaw]);
    // Lazy priming : if the module-level cache has ANY entry for
    // this appId (fresh OR stale), seed the initial state with it
    // so consumers paint immediately. Stale data still triggers a
    // background refresh below.
    const [state, setState] = SP_REACT.useState(() => {
        if (appId == null)
            return { data: null, loading: false, error: null };
        const cached = cache$2.get(appId);
        return {
            data: cached?.data ?? null,
            loading: cached?.data == null,
            error: null,
        };
    });
    const load = SP_REACT.useCallback(async (force) => {
        if (appId == null) {
            setState({ data: null, loading: false, error: null });
            return;
        }
        const cached = cache$2.get(appId);
        if (!force && cached && Date.now() - cached.ts < CACHE_TTL$1) {
            setState({ data: cached.data, loading: false, error: null });
            return;
        }
        // De-duplicate concurrent in-flight fetches
        if (cached?.inflight && !force) {
            const data = await cached.inflight;
            setState({ data, loading: false, error: null });
            return;
        }
        // Stale-while-revalidate : if we have ANY cached data,
        // keep showing it while the background refresh runs.
        setState((s) => ({
            data: s.data ?? cached?.data ?? null,
            loading: s.data == null && cached?.data == null,
            error: null,
        }));
        const promise = fetch(appId).then((data) => {
            cache$2.set(appId, { data, ts: Date.now(), inflight: null });
            return data;
        }, (err) => {
            cache$2.set(appId, { data: null, ts: Date.now(), inflight: null });
            throw err;
        });
        cache$2.set(appId, {
            data: cached?.data ?? null,
            ts: cached?.ts ?? 0,
            inflight: promise,
        });
        try {
            const data = await promise;
            setState({ data, loading: false, error: null });
        }
        catch (err) {
            setState({ data: null, loading: false, error: err });
        }
    }, [appId, fetch]);
    SP_REACT.useEffect(() => {
        void load(false);
    }, [load]);
    // Re-fetch when something invalidates this appId (download
    // complete / uninstall / cancel). Without this, the mounted
    // hook keeps its stale state — the Play section stays on the
    // wrong button until the page is reopened.
    SP_REACT.useEffect(() => {
        if (appId == null)
            return;
        return subscribeGameInfo(appId, () => {
            void load(true);
        });
    }, [appId, load]);
    const refresh = SP_REACT.useCallback(() => load(true), [load]);
    return { ...state, refresh };
}
/** Drop the cache entry for one appId so the next render
 *  re-fetches. Called after destructive actions (uninstall,
 *  cancel) where `is_installed` flips. Mirrors the legacy
 *  `gameInfoCache.delete(appId)` semantics — also drops the
 *  signed/unsigned variants since Steam shortcuts may be
 *  represented either way in the cache. */
function invalidateGameInfo(appId) {
    const signed = appId > 0x7fffffff ? appId - 0x100000000 : appId;
    const unsigned = appId < 0 ? appId + 0x100000000 : appId;
    // Clear the cache for every representation, then notify mounted
    // hooks so they refetch. The caller may pass either the signed
    // (backend Game.app_id) or unsigned (Steam shortcut) form, while
    // the mounted hook is keyed on whichever the page handed it — so
    // we fan out to both.
    for (const id of new Set([appId, signed, unsigned])) {
        cache$2.delete(id);
        notifyGameInfo(id);
    }
}

/**
 * Per-appId version counter for AppDetails injection keys.
 *
 * Bumping the version for an appId re-keys the injected
 * `<PlaySectionWrapper>` and `<GameInfoPanel>` so React fully
 * re-mounts them on game-state changes (install / uninstall).
 *
 * Lives in `lib/` rather than `views/AppDetailsPatch.tsx` so
 * hooks (`useGameActions`) can bump the version without
 * importing `views/`, which would create a cycle :
 *   useGameActions → views/AppDetailsPatch
 *     → components/play
 *       → PlaySectionWrapper → NotInstalledButtons → useGameActions.
 */
const versions = new Map();
/** Read the current version for an appId (0 by default). */
function getGameStateVersion(appId) {
    return versions.get(appId) ?? 0;
}
/** Increment the version so the next patch re-mounts the
 *  Unifideck overrides for this appId. Idempotent and cheap. */
function bumpGameStateVersion(appId) {
    versions.set(appId, (versions.get(appId) ?? 0) + 1);
}

/** Pull `need`/`free` GB out of an `insufficient_space:…` code. */
const SPACE_RE = /need=([\d.]+)GB,free=([\d.]+)GB/;
/** Strip a leading machine prefix like `legendary_exit_1: ` / `gogdl_exit_2: `. */
const EXIT_PREFIX_RE = /^\w+_exit_-?\d+:\s*/;
function friendlyDownloadError(raw, t) {
    if (!raw || !raw.trim()) {
        return t("errors.download.generic");
    }
    const lower = raw.toLowerCase();
    if (raw.startsWith("insufficient_space:")) {
        const m = SPACE_RE.exec(raw);
        if (m) {
            return t("errors.download.insufficientSpace", {
                need: m[1],
                free: m[2],
            });
        }
        return t("errors.download.diskSpace");
    }
    if (lower.includes("disk space") ||
        lower.includes("no space") ||
        lower.includes("disk full") ||
        raw.startsWith("low_space:")) {
        return t("errors.download.diskSpace");
    }
    if (lower.includes("network") || lower.includes("connection")) {
        return t("errors.download.network");
    }
    if (lower.includes("login") || lower.includes("auth")) {
        return t("errors.download.authExpired");
    }
    // Unknown failure — surface the real tail so nothing is ever silent.
    return raw.replace(EXIT_PREFIX_RE, "").trim() || t("errors.download.generic");
}

/**
 * Ubisoft shortcut launcher.
 *
 * Kept separate from the generic `authShortcutLaunch.ts`
 * because Ubisoft's auth flow is genuinely different :
 *
 *  - Reuses the existing Ubisoft Connect shortcut instead of
 *    creating a temporary one.
 *  - Saves and restores the user's proton tool after the
 *    auth run (so changing compat tool for the auth flow
 *    doesn't leak into the main game launch).
 *  - Extracts and re-injects user-supplied launch parameters
 *    around the auth env var so #%command% wrappers (mangohud,
 *    gamemoderun, etc.) are preserved.
 *
 * This file imports its shared types and helpers from
 * `lib/steam-bridge` (OP-F02e). The legacy version had its
 * own duplicated copies of those primitives; the unified
 * shortcut-types module made them centrally consumable.
 */
const RESTORE_POLL_DELAY_MS = 250;
const RESTORE_START_DELAY_MS = 500;
const RESTORE_TIMEOUT_MS = 5000;
const SHORTCUT_REGISTRATION_POLL_DELAY_MS = 250;
const AUTH_SHORTCUT_STORE_ID = "ubisoft:upc-auth";
const AUTH_PREFIX_NAME = ".upc-auth";
const LOG_TAG = "[UbisoftShortcutLaunch]";
const SHORTCUT_DISPLAY_NAME = "Ubisoft Connect";
/** Escape reg exp. */
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/** Strip Unifideck env tokens, the store_game_id, and the
 *  launcher path from the user's launch_options string so we
 *  keep only the user-supplied wrappers (mangohud, gamemoderun,
 *  #%command%, etc.). */
function extractUserParams(launchOptions, storeGameId, launcherPath) {
    let cleaned = launchOptions.replace(/\s*#%command%\s*$/g, "");
    const escaped = escapeRegExp(storeGameId);
    cleaned = cleaned.replace(/\bUNIFIDECK_[A-Z0-9_]+=(?:"[^"]*"|\S+)/g, "");
    cleaned = cleaned
        .replace(new RegExp(`"${escaped}"`, "g"), "")
        .replace(new RegExp(`(?<=^|\\s)${escaped}(?=\\s|$)`, "g"), "");
    if (launcherPath) {
        const escLauncher = escapeRegExp(launcherPath);
        cleaned = cleaned
            .replace(new RegExp(`"${escLauncher}"`, "g"), "")
            .replace(new RegExp(escLauncher, "g"), "");
    }
    return cleaned.replace(/\s{2,}/g, " ").trim();
}
/** Build temporary launch options. */
function buildTemporaryLaunchOptions(context, extraEnv, launchStoreGameId) {
    const sourceStoreGameId = context.store_game_id ?? "";
    const storeGameId = launchStoreGameId ?? sourceStoreGameId;
    const currentOptions = context.current_launch_options ?? sourceStoreGameId;
    const userParams = extractUserParams(currentOptions, sourceStoreGameId, context.launcher_path);
    const envTokens = Object.entries(extraEnv)
        .filter(([, value]) => Boolean(value))
        .map(([key, value]) => `${key}=${value}`)
        .join(" ");
    return [storeGameId, envTokens, userParams].filter(Boolean).join(" ").trim();
}
/** App store. */
function appStore() {
    return window.appStore;
}
/** Check whether shortcut registered. */
function isShortcutRegistered(appId) {
    return Boolean(appStore()?.m_mapApps?.get?.(appId));
}
/** Wait (at most ``minimumDelayMs``) for Steam to register the
 *  persistent shortcut in its in-memory app store.
 *
 *  Steam only loads ``shortcuts.vdf`` into ``appStore.m_mapApps`` at
 *  startup, so a shortcut the backend just wrote this session never
 *  appears here — there's no point polling a fixed timeout. We honour
 *  only the backend's ``minimumDelayMs`` hint (to cover the narrow
 *  window where Steam is still populating the map right after boot);
 *  if it's still not registered the caller falls back to a temporary
 *  shortcut created via ``AddShortcut``. */
async function waitForShortcutRegistration(appId, minimumDelayMs = 0) {
    if (isShortcutRegistered(appId))
        return;
    if (minimumDelayMs <= 0)
        return;
    const startedAt = Date.now();
    await new Promise((resolve) => {
        /** Poll. */
        const poll = () => {
            if (isShortcutRegistered(appId) ||
                Date.now() - startedAt >= minimumDelayMs) {
                resolve();
                return;
            }
            window.setTimeout(poll, SHORTCUT_REGISTRATION_POLL_DELAY_MS);
        };
        window.setTimeout(poll, SHORTCUT_REGISTRATION_POLL_DELAY_MS);
    });
}
/** Launch a Ubisoft action (auth or install) via a freshly-created
 *  throwaway shortcut. Used when the persistent "Ubisoft Connect"
 *  shortcut isn't yet in Steam's in-memory app store — the first
 *  session after the backend wrote it to ``shortcuts.vdf`` (Steam only
 *  loads that file at startup). ``RunGame`` on the unregistered
 *  persistent appid fails with Steam's "Game configuration unavailable"
 *  modal; ``AddShortcut`` registers an entry immediately and returns a
 *  real ``gameid``. The temp shortcut carries the same launch options,
 *  so the launcher routes identically, and is removed once its session
 *  ends. */
async function launchViaTemporaryShortcut(ctx, launchOptions) {
    const steamApps = window.SteamClient?.Apps;
    const launcherPath = ctx.launcher_path;
    if (!steamApps?.RunGame || !launcherPath) {
        return {
            success: false,
            error: "Steam launch APIs or launcher path unavailable",
        };
    }
    const tempAppId = await createTemporaryShortcut({
        appName: SHORTCUT_DISPLAY_NAME,
        launcherPath,
        launchOptions,
        logTag: LOG_TAG,
    });
    if (tempAppId === null) {
        return {
            success: false,
            error: "Ubisoft Connect could not be prepared in Steam. " +
                "Restart Steam once and try again.",
        };
    }
    const alreadyRunning = isShortcutAppRunning(tempAppId);
    try {
        steamApps.SpecifyCompatTool?.(tempAppId, ctx.tool_name ?? "");
        steamApps.SetShortcutLaunchOptions?.(tempAppId, launchOptions);
        steamApps.RunGame(getShortcutRunGameId(tempAppId), "", -1, 100);
        scheduleTemporaryShortcutCleanup(tempAppId, LOG_TAG);
        return { success: true, already_running: alreadyRunning };
    }
    catch (error) {
        console.error(`${LOG_TAG} temp shortcut launch failed:`, error);
        return {
            success: false,
            error: error instanceof Error ? error.message : "Failed to launch shortcut",
        };
    }
}
/** Schedule a post-launch restore : after Steam picks up the
 *  RunGame call we restore the user's saved compat tool and
 *  the original launch options. The restore polls until Steam
 *  reports the app as running, then waits a small grace
 *  period to avoid clobbering a still-applying RunGame. */
function scheduleLaunchStateRestore(appId, context, originalLaunchOptions) {
    const steamApps = window.SteamClient?.Apps;
    if (!steamApps)
        return;
    const startedAt = Date.now();
    const targetTool = context.saved_proton_tool ?? "";
    const tryRestore = () => {
        /** Try restore. */
        const elapsed = Date.now() - startedAt;
        if (elapsed < RESTORE_START_DELAY_MS) {
            window.setTimeout(tryRestore, RESTORE_POLL_DELAY_MS);
            return;
        }
        const running = isShortcutAppRunning(appId);
        if (!running && elapsed < RESTORE_TIMEOUT_MS) {
            window.setTimeout(tryRestore, RESTORE_POLL_DELAY_MS);
            return;
        }
        try {
            steamApps.SpecifyCompatTool?.(appId, targetTool);
            steamApps.SetShortcutLaunchOptions?.(appId, originalLaunchOptions);
        }
        catch (error) {
            console.error(`[UbisoftShortcutLaunch] Restore failed for appId=${appId}:`, error);
        }
    };
    window.setTimeout(tryRestore, RESTORE_START_DELAY_MS);
}
/** Launch a Ubisoft GAME via its existing shortcut, passing
 *  the install_id so the launcher knows which UPC entry to
 *  start. */
async function launchUbisoftInstallViaShortcut(storeGameId, extraEnv = {}, contextOverride, skipStateRestore = false) {
    const rawCtx = await call(rpcRoutes.getCompatToolForGame, storeGameId).catch(() => null);
    const baseCtx = rawCtx == null
        ? {}
        : unwrapRpcEnvelope(rawCtx, {
            route: rpcRoutes.getCompatToolForGame,
            throwing: false,
        });
    // ``contextOverride`` wins. The auth flow resolves the shortcut's
    // appid via ``get_ubisoft_auth_shortcut_context`` (which ensures the
    // shortcut exists and repairs the VDF); ``get_compat_tool_for_game``
    // only reads an already-registered game's compat setting and returns
    // no appid for the auth shortcut — hence the "Context unavailable"
    // regression when the auth path relied on it alone.
    const ctx = { ...baseCtx, ...contextOverride };
    console.log("[UbisoftShortcutLaunch] getCompatToolForGame raw:", rawCtx);
    console.log("[UbisoftShortcutLaunch] resolved ctx:", ctx);
    // The RPC envelope strips ``success`` from the data dict
    // (``_to_envelope`` moves it to the outer layer). Check
    // ``appid_unsigned`` directly — if the backend returned a
    // valid AppID the call succeeded regardless of whether a
    // ``success`` key survived the envelope unwrapping.
    if (!ctx.appid_unsigned) {
        console.error("[UbisoftShortcutLaunch] ctx.appid_unsigned is falsy:", ctx.appid_unsigned, "full ctx:", ctx);
        return { success: false, error: ctx?.error || "Context unavailable" };
    }
    const appId = ctx.appid_unsigned;
    console.log("[UbisoftShortcutLaunch] appId=%d, waiting for shortcut registration...", appId);
    await waitForShortcutRegistration(appId, ctx.launch_wait_ms ?? 0);
    const steamApps = window.SteamClient?.Apps;
    if (!steamApps?.RunGame || !steamApps?.SetShortcutLaunchOptions) {
        console.error("[UbisoftShortcutLaunch] Steam launch APIs unavailable: " +
            "RunGame=%s SetShortcutLaunchOptions=%s", typeof steamApps?.RunGame, typeof steamApps?.SetShortcutLaunchOptions);
        return { success: false, error: "Steam launch APIs unavailable" };
    }
    const originalOptions = ctx.current_launch_options ?? "";
    const tempOptions = buildTemporaryLaunchOptions(ctx, extraEnv, storeGameId);
    // First session after the backend wrote this shortcut to shortcuts.vdf:
    // Steam only loads that file into its in-memory app store at startup, so
    // RunGame on the persistent appid fails with "Game configuration
    // unavailable". Launch a throwaway shortcut instead (AddShortcut registers
    // it immediately); Steam picks up the persistent entry on the next restart,
    // after which the registered path below is used.
    if (!isShortcutRegistered(appId)) {
        console.log("[UbisoftShortcutLaunch] appId=%d not in Steam's app store; " +
            "launching via temporary shortcut", appId);
        return launchViaTemporaryShortcut(ctx, tempOptions);
    }
    const alreadyRunning = isShortcutAppRunning(appId);
    console.log("[UbisoftShortcutLaunch] RunGame(appId=%d, runGameId=%s, opts=%s)", appId, getShortcutRunGameId(appId), tempOptions);
    try {
        steamApps.SpecifyCompatTool?.(appId, ctx.tool_name ?? "");
        steamApps.SetShortcutLaunchOptions(appId, tempOptions);
        steamApps.RunGame(getShortcutRunGameId(appId), "", -1, 100);
        console.log("[UbisoftShortcutLaunch] RunGame called successfully");
        // The auth shortcut's temp options ARE its canonical persistent
        // options ("ubisoft:upc-auth UNIFIDECK_UBISOFT_ACTION=auth ..."), so
        // we must NOT restore them to the (empty) original — doing so wiped
        // the shortcut's LaunchOptions, leaving Steam to launch the bare
        // launcher with no args ("missing store:game_id" → the tile that
        // opens then instantly closes). Game installs still restore so the
        // user's wrappers / clean options come back.
        if (!skipStateRestore) {
            scheduleLaunchStateRestore(appId, ctx, originalOptions);
        }
        return { success: true, already_running: alreadyRunning };
    }
    catch (error) {
        console.error(`[UbisoftShortcutLaunch] launch failed:`, error);
        if (!skipStateRestore) {
            steamApps.SetShortcutLaunchOptions?.(appId, originalOptions);
        }
        return {
            success: false,
            error: error instanceof Error ? error.message : "Failed to launch shortcut",
        };
    }
}
/** Launch the Ubisoft AUTH flow via a dedicated auth shortcut
 *  (separate prefix to keep auth tokens away from the game
 *  prefix). The flow is otherwise the same as install. */
async function launchUbisoftAuthViaShortcut() {
    // Resolve (and ensure) the persistent auth shortcut's appid via the
    // dedicated context route — it scans/repairs the VDF and creates the
    // shortcut if missing. get_compat_tool_for_game (used by the install
    // path) cannot see the auth shortcut, so relying on it alone yielded
    // "Context unavailable". Mirrors staging's auth flow.
    const rawAuth = await call(rpcRoutes.getUbisoftAuthShortcutContext).catch(() => null);
    const authCtx = rawAuth == null
        ? undefined
        : unwrapRpcEnvelope(rawAuth, {
            route: rpcRoutes.getUbisoftAuthShortcutContext,
            throwing: false,
        });
    if (!authCtx?.appid_unsigned) {
        console.error("[UbisoftShortcutLaunch] auth shortcut context unavailable:", authCtx);
        return {
            success: false,
            error: authCtx?.error || "Auth shortcut not available",
        };
    }
    // Re-add UNIFIDECK_UBISOFT_ACTION=auth explicitly: buildTemporaryLaunchOptions
    // strips all UNIFIDECK_* tokens from the shortcut's stored options, so the
    // auth action must be re-supplied here or the launcher treats the run as a
    // game launch instead of a sign-in.
    return launchUbisoftInstallViaShortcut(AUTH_SHORTCUT_STORE_ID, {
        UNIFIDECK_UBISOFT_ACTION: "auth",
        UNIFIDECK_UBISOFT_PREFIX_NAME: AUTH_PREFIX_NAME,
    }, {
        appid_unsigned: authCtx.appid_unsigned,
        launch_wait_ms: authCtx.launch_wait_ms,
        launcher_path: authCtx.launcher_path,
    }, 
    // skipStateRestore: keep the canonical auth LaunchOptions on the
    // shortcut instead of wiping them back to empty after launch.
    true);
}

/**
 * download-store — boot-time singleton for download queue state.
 *
 * Replaces the QAM-bound EventBus subscriptions and initial
 * `get_download_queue` RPC that previously lived inside
 * `<DownloadProvider>`. The store:
 *
 *   1. Fetches the initial queue at boot via `get_download_queue`.
 *   2. Subscribes to all DOWNLOAD_* events to keep the snapshot
 *      current even while the QAM is closed.
 *   3. Handles `UBISOFT_INSTALL_LAUNCH_REQUESTED` — critical for
 *      opening UPC in Gaming Mode when the QAM is closed.
 *   4. Calls `invalidateGameInfo` + `bumpGameStateVersion` on
 *      terminal download events so AppDetails shows fresh state.
 *
 * React components subscribe via `useSyncExternalStore` through
 * the thin `<DownloadProvider>` wrapper (which now only provides
 * mutation actions and the reactive snapshot).
 */
// ── Helpers (moved from DownloadContext) ─────────────────
/** Pull the appId out of a DOWNLOAD_* terminal event payload. */
function extractAppId(payload) {
    if (!payload || typeof payload !== "object")
        return null;
    const game = payload.game;
    if (!game || typeof game !== "object")
        return null;
    const id = game.app_id;
    return typeof id === "number" ? id : null;
}
/** Pull the failure reason + title out of a `download_failed` payload.
 *  The event carries both `item.error_message` (the folded CLI tail)
 *  and a top-level `error` — prefer the item's message, fall back to
 *  the top-level one. */
function extractFailure(payload) {
    if (!payload || typeof payload !== "object")
        return {};
    const item = payload.item;
    const itemError = item && typeof item === "object"
        ? item.error_message
        : undefined;
    const topError = payload.error;
    const title = item && typeof item === "object"
        ? item.game_title
        : undefined;
    return {
        error: typeof itemError === "string" && itemError
            ? itemError
            : typeof topError === "string"
                ? topError
                : undefined,
        title: typeof title === "string" ? title : undefined,
    };
}
/** Build the `"<store>:<game_id>"` key from a terminal payload. */
function extractStoreGameId(payload) {
    if (!payload || typeof payload !== "object")
        return null;
    const item = payload
        .item;
    if (!item || typeof item !== "object")
        return null;
    const store = item.store;
    const gameId = item.game_id;
    if (typeof store !== "string" || typeof gameId !== "string")
        return null;
    return `${store}:${gameId}`;
}
/** Normalise the backend's queue shape to the frontend DTO. */
function adaptQueue(raw) {
    const obj = typeof raw === "object" && raw !== null
        ? raw
        : {};
    const queued = Array.isArray(obj.queued)
        ? obj.queued
        : [];
    const running = Array.isArray(obj.running)
        ? obj.running
        : [];
    const finished = Array.isArray(obj.finished)
        ? obj.finished
        : [];
    const current = obj.current ?? running[0] ?? null;
    return {
        success: true,
        queued,
        finished,
        current,
        state: running.length > 0 ? "running" : "idle",
    };
}
class DownloadStoreImpl {
    constructor() {
        this._snapshot = { queue: null, loading: true };
        this._listeners = new Set();
        this._unsubs = [];
        this._ubisoftLaunched = new Set();
        // ── useSyncExternalStore API ──────────────────────────
        this.getSnapshot = () => this._snapshot;
        this.subscribe = (listener) => {
            this._listeners.add(listener);
            return () => this._listeners.delete(listener);
        };
    }
    /** Start subscriptions and initial fetch. */
    start() {
        void this._fetchQueue();
        this._unsubs.push(EventBusClient.subscribe("download_queued", () => this._fetchQueue()));
        this._unsubs.push(EventBusClient.subscribe("download_started", () => {
            EventBusClient.bumpToFast();
            void this._fetchQueue();
        }));
        this._unsubs.push(EventBusClient.subscribe("download_progress", (payload) => {
            this._setSnapshot((prev) => ({
                ...prev,
                queue: prev.queue && {
                    ...prev.queue,
                    current: prev.queue.current && {
                        ...prev.queue.current,
                        progress_percent: payload.progress ??
                            prev.queue.current.progress_percent,
                        speed_mbps: payload.speed_mbps ?? prev.queue.current.speed_mbps,
                        eta_seconds: payload.eta_seconds ??
                            prev.queue.current.eta_seconds,
                    },
                },
            }));
        }));
        const onTerminal = (payload) => {
            const appId = extractAppId(payload);
            if (appId != null) {
                invalidateGameInfo(appId);
                bumpGameStateVersion(appId);
            }
            const storeGameId = extractStoreGameId(payload);
            if (storeGameId)
                this._ubisoftLaunched.delete(storeGameId);
            void this._fetchQueue();
        };
        // A failed install used to surface only as a bare red "Failed" badge —
        // the backend already folds the real error into the payload, so toast it.
        const onFailed = (payload) => {
            onTerminal(payload);
            const { error, title } = extractFailure(payload);
            if (!error)
                return;
            const body = friendlyDownloadError(error, instance.t.bind(instance));
            try {
                toaster.toast({
                    title: title
                        ? `${instance.t("toasts.downloadFailed")}: ${title}`
                        : instance.t("toasts.downloadFailed"),
                    body,
                    duration: 7500,
                });
            }
            catch {
                console.error(`[DownloadStore] install failed: ${title ?? ""} — ${body}`);
            }
        };
        this._unsubs.push(EventBusClient.subscribe("download_complete", onTerminal));
        this._unsubs.push(EventBusClient.subscribe("download_failed", onFailed));
        this._unsubs.push(EventBusClient.subscribe("download_cancelled", onTerminal));
        // Ubisoft install — open UPC via Steam's RunGame
        this._unsubs.push(EventBusClient.subscribe("ubisoft_install_launch_requested", (payload) => {
            const storeGameId = payload
                .store_game_id;
            if (typeof storeGameId !== "string" || !storeGameId)
                return;
            if (this._ubisoftLaunched.has(storeGameId))
                return;
            this._ubisoftLaunched.add(storeGameId);
            void launchUbisoftInstallViaShortcut(storeGameId, {
                UNIFIDECK_UBISOFT_ACTION: "install",
            }).then((result) => {
                if (!result.success) {
                    this._ubisoftLaunched.delete(storeGameId);
                    console.error("[DownloadStore] Ubisoft UPC RunGame failed:", result.error);
                }
            });
        }));
    }
    /** Stop all subscriptions. */
    stop() {
        for (const unsub of this._unsubs)
            unsub();
        this._unsubs = [];
    }
    /** Re-fetch the queue from the backend. */
    async refetch() {
        await this._fetchQueue();
    }
    // ── Internals ─────────────────────────────────────────
    async _fetchQueue() {
        try {
            const raw = await call(rpcRoutes.getDownloadQueue);
            const data = unwrapRpcEnvelope(raw, {
                route: rpcRoutes.getDownloadQueue,
                throwing: false,
            });
            this._setSnapshot({ queue: adaptQueue(data), loading: false });
        }
        catch (e) {
            console.warn("[DownloadStore] fetch failed:", e);
        }
    }
    _setSnapshot(update) {
        const next = typeof update === "function" ? update(this._snapshot) : update;
        this._snapshot = next;
        this._emit();
    }
    _emit() {
        for (const listener of this._listeners)
            listener();
    }
}
/** Singleton — started at boot from `definePlugin`. */
const downloadStore = new DownloadStoreImpl();

const Ctx = SP_REACT.createContext(null);
/**
 * Provider that exposes the download queue and mutation actions.
 * Queue state comes from the boot-time `downloadStore` singleton
 * via `useSyncExternalStore` — no mount-time RPC or EventBus
 * subscriptions happen here.
 */
const DownloadProvider = ({ children }) => {
    const { queue, loading } = SP_REACT.useSyncExternalStore(downloadStore.subscribe, downloadStore.getSnapshot);
    // RPC mutations — user-initiated, stay in the React layer
    const installMut = useRPCMutation(rpcRoutes.installGame);
    const uninstallMut = useRPCMutation(rpcRoutes.uninstallGame);
    const cancelMut = useRPCMutation(rpcRoutes.cancelDownload);
    const refresh = SP_REACT.useCallback(async () => {
        await downloadStore.refetch();
    }, []);
    const installGame = SP_REACT.useCallback((store, gameId, options) => installMut.mutate(store, gameId, options), [installMut]);
    const uninstallGame = SP_REACT.useCallback((appId, deletePrefix = false) => uninstallMut.mutate(appId, deletePrefix), [uninstallMut]);
    const cancelDownload = SP_REACT.useCallback((downloadId) => {
        const idx = downloadId.indexOf(":");
        const store = idx > 0 ? downloadId.slice(0, idx) : "";
        const gameId = idx > 0 ? downloadId.slice(idx + 1) : downloadId;
        return cancelMut.mutate(store, gameId);
    }, [cancelMut]);
    const value = {
        queue,
        loading,
        installGame,
        uninstallGame,
        cancelDownload,
        refresh,
    };
    return SP_JSX.jsx(Ctx.Provider, { value: value, children: children });
};
/**
 * Access the DownloadContext value. Throws if used
 * outside `<DownloadProvider>`.
 *
 * @throws Error when the provider is missing.
 */
function useDownloads() {
    const v = SP_REACT.useContext(Ctx);
    if (!v)
        throw new Error("useDownloads called outside <DownloadProvider>");
    return v;
}

/**
 * Composition of all five context providers in the order
 * dictated by their dependency graph (Locale →
 * Store → Auth → Sync → Download). Mounted once at the
 * top of the React tree by the plugin entry point.
 */
const RootProvider = ({ children }) => {
    return (SP_JSX.jsx(LocaleProvider, { children: SP_JSX.jsx(StoreProvider, { children: SP_JSX.jsx(LibraryProvider, { children: SP_JSX.jsx(AuthProvider, { children: SP_JSX.jsx(SyncProvider, { children: SP_JSX.jsx(DownloadProvider, { children: children }) }) }) }) }) }));
};

/**
 * useToast — typed wrapper around Decky's `toaster.toast`.
 *
 * Wraps the global toaster with three semantics-bearing
 * methods (`info`, `success`, `error`) and a base `show` for
 * full control. Components import this hook instead of
 * `toaster` directly so :
 *  - duration defaults are centralised (no scattered "5000")
 *  - a future swap to a different notification system is a
 *    one-file change
 *  - a no-op fallback is provided when the @decky/api
 *    toaster is unavailable (test environments)
 */

const DEFAULT_DURATION$2 = 5000;
const ERROR_DURATION$1 = 7500;
/**
 * Hook returning a memoised `showToast` callback that
 * forwards to Decky Loader's toast API. Centralised
 * here so styling / duration defaults stay consistent
 * across the codebase and so test code can mock a
 * single boundary.
 */
function useToast() {
    const show = SP_REACT.useCallback((title, body, options = {}) => {
        try {
            toaster.toast({
                title,
                body: body ?? options.body ?? "",
                duration: options.duration ?? DEFAULT_DURATION$2,
                onClick: options.onClick,
            });
        }
        catch {
            console.log(`[Toast] ${title}: ${body ?? ""}`);
        }
    }, []);
    const info = SP_REACT.useCallback((title, body) => show(title, body), [show]);
    const success = SP_REACT.useCallback((title, body) => show(title, body), [show]);
    const error = SP_REACT.useCallback((title, body) => show(title, body, { duration: ERROR_DURATION$1 }), [show]);
    return { show, info, success, error };
}

/**
 * Controller-aware launch wrapper.
 *
 * An earlier programmatic controller-layout approach (driving Steam's
 * Controller Configurator popup) was reverted: it raced Steam's own
 * configurator and threw "Unknown method" errors. The replacement here
 * uses the typed `SteamClient.Input.SetSelectedConfigForApp` setter
 * directly — method names + signature verified against the Steam UI
 * bundle (`steamui/*.js`), and the call confirmed working in Gaming Mode.
 *
 * Two layouts are applied, both off the live template stream:
 *   • Auth/login window → "Web Browser" (keyboard/mouse) so the in-prefix
 *     login UI is navigable — see `applyWebBrowserLayout`.
 *   • xCloud game launch → "Gamepad With Joystick Trackpad" so the Deck
 *     controller drives the streamed game as an Xbox pad — see
 *     `applyGamepadLayout` / `ensureGamepadConfigForApp`. This is the
 *     layout Microsoft's official Steam Deck + Edge xCloud guide mandates;
 *     without it the default keyboard/mouse layout leaves menus navigable
 *     (trackpad-mouse) but sends no gamepad input to the streamed game.
 *
 * Standards compliance : the previous version reached into
 * `(window as any).appStore` directly, bypassing the
 * SteamBridge isolation layer. This refactor delegates to
 * `getShortcutRunGameId` which lives inside SteamBridge —
 * any future change to the Steam internal path is then
 * a one-file edit.
 *
 * ── Investigation (2026-06, beta feedback) ──────────────────
 * Beta testers asked for the Unifideck-Launcher / auth-window
 * shortcut to come up with a "Web Browser" controller layout
 * (keyboard/mouse-like bindings) so the in-prefix login UI is
 * navigable. Re-checked the available surface:
 *
 *   • `@decky/ui` ships NO typed `SteamClient.Input.*` surface,
 *     and our `types/steam.ts` declares only `Apps` +
 *     `GameSessions`. The vestigial `ControllerConfigInfo*`
 *     types there describe the read-only template-list *stream*
 *     — there is no verified setter binding.
 *   • The only controller method on the typed `Apps` surface is
 *     `ShowControllerConfigurator(appId)`, which pops Steam's
 *     full configurator UI — too intrusive to fire mid-auth.
 *   • The historical programmatic path (set-active-config /
 *     template apply) is the exact code that raced Steam and
 *     threw "Unknown method"; it was deliberately reverted.
 *
 * Update (2026-06): a typed `SteamClient.Input` setter surface WAS
 * confirmed. `RegisterForControllerConfigInfoMessages` +
 * `QueryControllerConfigsForApp` + `SetSelectedConfigForApp` were verified
 * against the Steam UI bundle and exercised live in Gaming Mode (the
 * "Unknown method" failures came from the old configurator-popup path, not
 * these). Rather than hardcode the template URL (produced at runtime) we
 * enumerate the live config list and pick the matching official entry
 * (`controller_neptune_webbrowser.vdf` for auth,
 * `controller_neptune_gamepad_fps.vdf` for xCloud). Every call is wrapped
 * so any failure leaves the launch unaffected; applied BEFORE `RunGame`.
 */
const LOG_PREFIX = "[ControllerConfig]";
// The Deck's built-in controller is index 0 in Gaming Mode.
const PRIMARY_CONTROLLER_INDEX = 0;
// How long to wait for Steam to stream the template list before giving up.
const CONFIG_INFO_TIMEOUT_MS = 4000;
/** Default an xCloud shortcut to the "Gamepad With Joystick Trackpad"
 *  layout. Best-effort and fire-and-forget: a failed/absent apply just
 *  leaves the current layout. Run *after* `RunGame` (see
 *  `launchAppWithConfiguredGamepad`) — Steam's controller-config API is
 *  inert for an idle shortcut, so we apply once the app is the active
 *  launch target; the selection persists for subsequent launches. */
async function ensureGamepadConfigForApp(appId) {
    const applied = await applyGamepadLayout(appId);
    console.log(`${LOG_PREFIX} gamepad layout for appId=${appId}: ` +
        `${applied ? "applied" : "kept default"}`);
}
/** Launch a Steam shortcut by appId, going through Steam's
 *  RunGame API. Returns false when Steam's Apps surface is
 *  unavailable (test environments, very early plugin boot). */
async function launchAppWithConfiguredGamepad(appId) {
    const steamApps = window.SteamClient?.Apps;
    if (!steamApps?.RunGame) {
        return false;
    }
    steamApps.RunGame(getShortcutRunGameId(appId), "", -1, 100);
    console.log(`${LOG_PREFIX} Launched appId=${appId}`);
    // Fire-and-forget so it never delays the launch: default the shortcut to
    // the gamepad layout once it's the active controller-config target.
    void ensureGamepadConfigForApp(appId);
    return true;
}
/** A config-info message that carries a template (vs. a "Done" marker). */
function isTemplateEntry(m) {
    return ("URL" in m && typeof m.URL === "string");
}
/** The official Steam "Web Browser" template (by title or filename). */
function isWebBrowserTemplate(m) {
    return (m.bOfficial && (m.Title === "Web Browser" || /webbrowser/i.test(m.URL)));
}
/**
 * The official "Gamepad With Joystick Trackpad" template
 * (`controller_neptune_gamepad_fps.vdf`). This is the layout Microsoft's
 * own "Xbox Cloud Gaming in Microsoft Edge with Steam Deck" guide tells
 * users to apply: it maps the Deck's physical controls to the virtual
 * Xbox pad (right trackpad → right stick) so xCloud forwards real gamepad
 * input to the streamed game. Without it the Deck defaults to a
 * keyboard/mouse layout — menus stay navigable via the trackpad-mouse but
 * the game itself receives no controller input. Matched by filename
 * (stable) or localized title.
 */
function isGamepadJoystickTemplate(m) {
    return (m.bOfficial &&
        (/gamepad_fps/i.test(m.URL) || /gamepad\s+with\s+joystick/i.test(m.Title)));
}
/**
 * Shared core: stream the app's controller configs, pick the first
 * official template matching `matches`, and persist it for the app via
 * `SetSelectedConfigForApp`. Resolves `true` when a template was applied,
 * `false` on unavailable API / no match / timeout / error. Never throws.
 *
 * Flow (all verified against the Steam UI bundle, and the live API
 * confirmed callable in Gaming Mode):
 *   1. Register for the app's controller-config info stream.
 *   2. `QueryControllerConfigsForApp` to make Steam emit it.
 *   3. Pick the matching official entry and apply its `URL` via
 *      `SetSelectedConfigForApp` (which persists the selection).
 *
 * Fully guarded and non-blocking: any failure leaves the launch
 * completely unaffected (the app still launches, just on the default
 * layout). A timeout unregisters the listener if Steam never streams a
 * match. Call this BEFORE `RunGame` (and await it if the current launch
 * must pick up the layout).
 */
function applySelectedTemplate(appId, matches, label) {
    return new Promise((resolve) => {
        try {
            const input = window.SteamClient?.Input;
            if (!input?.RegisterForControllerConfigInfoMessages ||
                !input?.QueryControllerConfigsForApp ||
                !input?.SetSelectedConfigForApp) {
                console.log(`${LOG_PREFIX} SteamClient.Input config API unavailable — ` +
                    `default layout kept for appId=${appId}`);
                resolve(false);
                return;
            }
            const idx = PRIMARY_CONTROLLER_INDEX;
            // Mutable holder so `finish` can close over the registration +
            // timer that are created after it (avoids a let/const cycle).
            const state = { settled: false };
            const finish = (ok) => {
                if (state.timer !== undefined)
                    clearTimeout(state.timer);
                try {
                    state.reg?.unregister();
                }
                catch {
                    /* ignore */
                }
                resolve(ok);
            };
            state.reg = input.RegisterForControllerConfigInfoMessages(appId, (messages) => {
                if (state.settled || !Array.isArray(messages))
                    return;
                const tpl = messages.filter(isTemplateEntry).find(matches);
                if (!tpl)
                    return;
                state.settled = true;
                try {
                    input.SetSelectedConfigForApp(appId, idx, tpl.URL, false, true);
                    console.log(`${LOG_PREFIX} applied ${label} to ` +
                        `appId=${appId} (${tpl.URL})`);
                    finish(true);
                }
                catch (e) {
                    console.warn(`${LOG_PREFIX} SetSelectedConfigForApp failed:`, e);
                    finish(false);
                }
            });
            input.QueryControllerConfigsForApp(appId, idx, false);
            state.timer = setTimeout(() => {
                if (!state.settled) {
                    console.log(`${LOG_PREFIX} ${label} template not found for ` +
                        `appId=${appId} within ${CONFIG_INFO_TIMEOUT_MS}ms — ` +
                        `default layout kept`);
                }
                finish(false);
            }, CONFIG_INFO_TIMEOUT_MS);
        }
        catch (e) {
            console.warn(`${LOG_PREFIX} applySelectedTemplate error (ignored):`, e);
            resolve(false);
        }
    });
}
/**
 * Apply the official Steam "Web Browser" controller template to the
 * temporary auth-window shortcut so the store-login page is navigable
 * with the trackpad/stick (mouse) instead of a useless gamepad binding.
 * Fire-and-forget: applied AFTER the temp shortcut's app entry exists and
 * BEFORE `RunGame`; failure leaves login unaffected.
 */
function applyWebBrowserLayout(appId) {
    void applySelectedTemplate(appId, isWebBrowserTemplate, "Web Browser layout");
}
/**
 * Apply the official "Gamepad With Joystick Trackpad" template to an
 * xCloud shortcut so the Steam Deck controller drives the streamed game
 * as an Xbox pad (see `isGamepadJoystickTemplate`). Resolves once the
 * layout is applied (or the attempt times out) so the caller can await it
 * before `RunGame`, ensuring the current launch picks up the layout. The
 * selection persists, so later launches keep it.
 */
function applyGamepadLayout(appId) {
    return applySelectedTemplate(appId, isGamepadJoystickTemplate, "Gamepad (Joystick Trackpad) layout");
}

/**
 * Generic auth-via-shortcut launcher.
 *
 * Drives the Steam shortcut → unifideck-launcher pipeline
 * for every store whose auth flow follows the
 * "create temporary shortcut → RunGame → cleanup" pattern :
 *
 *     Epic    : UNIFIDECK_EPIC_ACTION=auth      → Legendary
 *     GOG     : UNIFIDECK_GOG_ACTION=auth       → gogdl
 *     Amazon  : UNIFIDECK_AMAZON_ACTION=auth    → Nile
 *     Microsoft: UNIFIDECK_MICROSOFT_ACTION=auth → Chromium + OAuth URL
 *
 * The frontend behaviour is identical for all four : the
 * differences (CLI invocation, browser launch with OAuth URL)
 * happen entirely inside the unifideck-launcher binary based
 * on the env var. Microsoft was a separate file in the
 * legacy code with copy-pasted polling helpers and a subtly
 * different result type ; this module unifies the four into
 * a single generic launcher driven by `AuthShortcutConfig`.
 *
 * Ubisoft auth uses a different flow (reuses an existing
 * shortcut, restores the proton tool afterwards) and lives
 * in `ubisoftShortcutLaunch.ts`.
 */
const EPIC_AUTH_CONFIG = {
    store: "epic",
    tempStoreIdPrefix: "epic:epic-auth-temp",
    appName: "Epic Games Sign-In",
    actionEnvVar: "UNIFIDECK_EPIC_ACTION",
    contextRpcMethod: rpcRoutes.getEpicAuthShortcutContext,
};
const GOG_AUTH_CONFIG = {
    store: "gog",
    tempStoreIdPrefix: "gog:gog-auth-temp",
    appName: "GOG Sign-In",
    actionEnvVar: "UNIFIDECK_GOG_ACTION",
    contextRpcMethod: rpcRoutes.getGogAuthShortcutContext,
};
const AMAZON_AUTH_CONFIG = {
    store: "amazon",
    tempStoreIdPrefix: "amazon:amazon-auth-temp",
    appName: "Amazon Games Sign-In",
    actionEnvVar: "UNIFIDECK_AMAZON_ACTION",
    contextRpcMethod: rpcRoutes.getAmazonAuthShortcutContext,
};
const MICROSOFT_AUTH_CONFIG = {
    store: "microsoft",
    tempStoreIdPrefix: "microsoft:ms-auth-temp",
    appName: "Microsoft Sign-In",
    actionEnvVar: "UNIFIDECK_MICROSOFT_ACTION",
    contextRpcMethod: rpcRoutes.getMicrosoftAuthShortcutContext,
};
/** Log tag. */
function logTag(config) {
    return `[AuthShortcutLaunch:${config.store}]`;
}
/**
 * Generic auth-shortcut launcher used by all stores that
 * capture credentials inside a Wine prefix (Epic, GOG,
 * Amazon, Microsoft). Always creates a fresh ephemeral
 * non-Steam shortcut, asks Steam to launch it through the
 * launcher binary, then removes the shortcut after a 15s
 * grace period so the user's library never accumulates
 * "Sign-In" entries.
 *
 * No persistent path: the previous implementation tried to
 * reuse a backend-written shortcut, but Steam's in-memory
 * app store is only populated from shortcuts.vdf at Steam
 * startup, so backend writes mid-session weren't visible.
 * Whichever stores happened to have their persistent
 * entry loaded by Steam (typically only Epic) launched
 * with the "real game" tile + cover art, while the rest
 * fell through to the temp path. Going temp-only makes
 * every store behave the same.
 */
async function launchAuthViaShortcut(config) {
    const tag = logTag(config);
    console.log(`${tag} Starting auth shortcut launch flow`);
    const raw = await call(config.contextRpcMethod);
    const ctx = unwrapRpcEnvelope(raw, {
        route: config.contextRpcMethod,
        throwing: false,
    });
    if (!ctx?.launcher_path) {
        console.error(`${tag} Auth context failed — envelope:`, raw, `unwrapped:`, ctx);
        return {
            success: false,
            error: ctx?.error || "Auth launcher path unavailable",
        };
    }
    const steamApps = window.SteamClient?.Apps;
    if (!steamApps?.RunGame || !steamApps?.SetShortcutLaunchOptions) {
        return {
            success: false,
            error: "Steam shortcut launch APIs are unavailable",
        };
    }
    const tempStoreId = `${config.tempStoreIdPrefix}-${Date.now()}`;
    const tempLaunchOptions = `${tempStoreId} ${config.actionEnvVar}=auth`;
    const tempAppId = await createTemporaryShortcut({
        appName: config.appName,
        launcherPath: ctx.launcher_path,
        launchOptions: tempLaunchOptions,
        logTag: logTag(config),
    });
    if (tempAppId === null) {
        return {
            success: false,
            error: `${config.appName} could not be prepared in Steam. ` +
                `Restart Steam once and try again.`,
        };
    }
    const alreadyRunning = isShortcutAppRunning(tempAppId);
    try {
        steamApps.SpecifyCompatTool?.(tempAppId, "");
        // Best-effort: give the login window a keyboard/mouse layout so the
        // store sign-in page is navigable. Fully guarded — never blocks the
        // launch (see applyWebBrowserLayout).
        applyWebBrowserLayout(tempAppId);
        steamApps.SetShortcutLaunchOptions(tempAppId, tempLaunchOptions);
        const runGameId = getShortcutRunGameId(tempAppId);
        console.log(`${tag} Calling RunGame: appId=${tempAppId}, ` +
            `runGameId=${runGameId}, launchOpts="${tempLaunchOptions}"`);
        steamApps.RunGame(runGameId, "", -1, 100);
        scheduleTemporaryShortcutCleanup(tempAppId, logTag(config));
        console.log(`${tag} Auth launched via RunGame (appId=${tempAppId})`);
        return { success: true, already_running: alreadyRunning, appId: tempAppId };
    }
    catch (error) {
        console.error(`${tag} Shortcut launch failed:`, error);
        return {
            success: false,
            error: error instanceof Error ? error.message : "Failed to launch shortcut",
        };
    }
}
// ─── Pre-configured store launchers ──────────────────────────
/**
 * Epic Games specialisation of
 * {@link launchAuthViaShortcut}. Pre-bound with the
 * Epic prefix path, the legendary login URL and the
 * Epic-specific session capture file name.
 */
const launchEpicAuthViaShortcut = () => launchAuthViaShortcut(EPIC_AUTH_CONFIG);
/**
 * GOG Galaxy specialisation of
 * {@link launchAuthViaShortcut}. Pre-bound with the
 * GOG prefix path, the OAuth login URL and the
 * GOG-specific session capture file name.
 */
const launchGogAuthViaShortcut = () => launchAuthViaShortcut(GOG_AUTH_CONFIG);
/**
 * Amazon Games specialisation of
 * {@link launchAuthViaShortcut}. Pre-bound with the
 * Amazon prefix path, the nile login URL and the
 * Amazon-specific session capture file name.
 */
const launchAmazonAuthViaShortcut = () => launchAuthViaShortcut(AMAZON_AUTH_CONFIG);
/**
 * Microsoft / xCloud specialisation of
 * {@link launchAuthViaShortcut}. Pre-bound with the
 * Edge prefix path, the OAuth login URL and the
 * Microsoft-specific session capture file name.
 */
const launchMicrosoftAuthViaShortcut = () => launchAuthViaShortcut(MICROSOFT_AUTH_CONFIG);

/**
 * AuthDispatcher — frontend-side auth orchestrator.
 *
 * Owns the full per-store auth handshake end-to-end :
 *
 *   1. ``store_auth(store, "start")`` on the backend so it
 *      creates/refreshes the auth shortcut, writes its
 *      auth-URL file, starts its session monitor, etc.
 *   2. ``launch<Store>AuthViaShortcut()`` so the user actually
 *      sees the browser / launcher (Steam's ``RunGame()``).
 *   3. Subscribes to the backend EventBus
 *      (``STORE_AUTH_COMPLETE`` / ``STORE_AUTH_FAILED``) so
 *      callers can `await` the final outcome.
 *
 * Components / hooks call ``AuthDispatcher.start(store)`` and
 * get a single Promise back. The legacy
 * ``dispatch_unifideck_action("unifideck://auth/<store>")``
 * channel is kept as the way the *backend* talks to itself
 * from toast-action buttons ; UI-initiated connects use the
 * direct ``store_auth`` RPC because that's the only path that
 * returns the per-store ``AuthResult`` we toast on completion.
 *
 * Mutex : only one auth flow at a time. A second `start()`
 * for the same store while another is in flight returns the
 * in-flight promise ; for a different store, it rejects.
 */
const AUTH_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes ceiling
/** Auth dispatcher impl. */
class AuthDispatcherImpl {
    constructor() {
        /** Per-store in-flight auth: allows concurrent auth for
         *  DIFFERENT stores (e.g. user starts GOG then clicks
         *  Microsoft — both run in parallel). Only deduplicates
         *  the SAME store (clicking Connect twice returns the
         *  existing promise). */
        this.inflight = new Map();
    }
    /** Start the auth flow for `store`. Resolves when the
     *  backend emits `STORE_AUTH_COMPLETE` / `STORE_AUTH_FAILED`
     *  for that store, or rejects on timeout / shortcut launch
     *  failure. */
    async start(store) {
        const existing = this.inflight.get(store);
        if (existing)
            return existing;
        EventBusClient.bumpToFast();
        const promise = this.runFlow(store);
        this.inflight.set(store, promise);
        promise.finally(() => {
            this.inflight.delete(store);
        });
        return promise;
    }
    /**
     * Internal coroutine that owns one auth flow end-to-end :
     *  - subscribe to STORE_AUTH_COMPLETE / STORE_AUTH_FAILED
     *  - kick the backend `store_auth` RPC
     *  - launch the auth shortcut so the user sees the flow
     *  - resolve / reject + dispose every listener.
     */
    async runFlow(store) {
        return new Promise((resolve, reject) => {
            /** Cleanup. */
            const cleanup = [];
            /** Timer. */
            const timer = setTimeout(() => {
                for (const fn of cleanup)
                    fn();
                reject(new Error(`auth timeout: ${store}`));
            }, AUTH_TIMEOUT_MS);
            cleanup.push(() => clearTimeout(timer));
            /** On resolved. */
            const onResolved = (result) => {
                for (const fn of cleanup)
                    fn();
                if (result.success) {
                    // Fire-and-forget: a fresh login should make the store
                    // available for sync immediately, not just after the next
                    // restart. Queues behind an in-flight sync on the backend
                    // (SyncService._enqueue) rather than blocking this Promise —
                    // callers resolve as soon as auth completes, same as before.
                    void call(rpcRoutes.requestAuthSync, store).catch((e) => {
                        console.error(`[AuthDispatcher:${store}] requestAuthSync failed:`, e);
                    });
                }
                resolve(result);
            };
            cleanup.push(EventBusClient.subscribe(Events.STORE_AUTH_COMPLETE, (raw) => {
                const p = raw;
                if (p.store !== store)
                    return;
                onResolved({ success: true, store });
            }));
            cleanup.push(EventBusClient.subscribe(Events.STORE_AUTH_FAILED, (raw) => {
                const p = raw;
                if (p.store !== store)
                    return;
                onResolved({
                    success: false,
                    store,
                    error: p.error ?? "unknown auth failure",
                });
            }));
            // Fire the kick + shortcut launch only after the
            // listeners are installed — otherwise a fast backend
            // flow could emit its terminal event before we
            // subscribe.
            void this.kickAndLaunch(store)
                .then((early) => {
                // Fast-path : the backend's ``store_auth`` returned
                // ``success: true`` right away (already-authed user).
                // Don't wait for an EventBus echo — the event may
                // race the RPC response and arrive before our poll
                // tick, leaving the Promise hung. Resolve directly.
                if (early)
                    onResolved(early);
            })
                .catch((e) => {
                for (const fn of cleanup)
                    fn();
                reject(e);
            });
        });
    }
    /** Two-stage kick : backend prep then frontend shortcut
     *  launch.
     *
     *  Returns an ``AuthResult`` only on the **fast path** —
     *  i.e. when ``store_auth`` reports ``success: true``
     *  immediately (the store's fast-path detected existing
     *  valid tokens). In that case the caller resolves the
     *  Promise directly with the returned result : the
     *  backend's ``STORE_AUTH_COMPLETE`` event may race the
     *  RPC response (the EventBus polls every 250-2000ms), so
     *  waiting for it would hang the UI at "Working…".
     *
     *  Returns ``null`` on the slow path — the shortcut has
     *  been kicked, and the surrounding ``runFlow`` waits for
     *  ``STORE_AUTH_COMPLETE`` / ``STORE_AUTH_FAILED`` events
     *  to resolve.
     *
     *  Throws if either stage fails outright (so ``runFlow``
     *  rejects the Promise).
     */
    async kickAndLaunch(store) {
        console.log(`[AuthDispatcher:${store}] backend prep via store_auth`);
        const raw = await call(rpcRoutes.storeAuth, store, "start");
        const startResult = unwrapRpcEnvelope(raw, {
            route: rpcRoutes.storeAuth,
            throwing: false,
        });
        console.log(`[AuthDispatcher:${store}] store_auth returned:`, startResult);
        if (startResult?.success === true &&
            !startResult?.metadata
                ?.pending) {
            console.log(`[AuthDispatcher:${store}] backend reports already-authed, ` +
                `resolving without shortcut launch`);
            return { success: true, store };
        }
        // Backend reported a structured failure (e.g. ``edge_not_installed``)
        // before any shortcut was needed. Surface it as the resolved
        // AuthResult so the caller (useStoreAuth) can react — show the
        // Chromium install modal, surface a toast, etc. — instead of
        // firing a useless shortcut launch the user can't complete.
        if (startResult && startResult.success === false && startResult.error) {
            console.log(`[AuthDispatcher:${store}] backend rejected start: ` +
                `${startResult.error} — skipping shortcut launch`);
            return {
                success: false,
                store,
                error: startResult.error,
            };
        }
        console.log(`[AuthDispatcher:${store}] launching shortcut`);
        const launchResult = await this.launchForStore(store);
        console.log(`[AuthDispatcher:${store}] shortcut launch result:`, launchResult);
        if (!launchResult.success) {
            throw new Error(launchResult.error ?? `${store} auth shortcut failed to launch`);
        }
        // Slow path : shortcut launched, wait for the backend's
        // terminal event to land on the EventBus.
        return null;
    }
    /** Dispatch to the per-store shortcut launcher. */
    async launchForStore(store) {
        switch (store) {
            case "epic":
                return launchEpicAuthViaShortcut();
            case "gog":
                return launchGogAuthViaShortcut();
            case "amazon":
                return launchAmazonAuthViaShortcut();
            case "microsoft":
                return launchMicrosoftAuthViaShortcut();
            case "ubisoft":
                return launchUbisoftAuthViaShortcut();
            default:
                return { success: false, error: `no launcher wired for ${store}` };
        }
    }
}
/** Singleton — auth flows are mutually exclusive by nature. */
const AuthDispatcher = new AuthDispatcherImpl();

/** Three-state install UI : idle (Install / Cancel), in-flight
 *  (Spinner), and result (toast + close). Buttons are disabled
 *  while a call is in flight so the user can't double-click. */
const ChromiumInstallModal = ({ onInstalled, closeModal, }) => {
    const { t } = useTranslation();
    const toast = useToast();
    const [installing, setInstalling] = SP_REACT.useState(false);
    const [installed, setInstalled] = SP_REACT.useState(false);
    const [error, setError] = SP_REACT.useState(null);
    const handleInstall = async () => {
        setInstalling(true);
        setError(null);
        try {
            const raw = await call(rpcRoutes.installEdge);
            const result = unwrapRpcEnvelope(raw, {
                route: rpcRoutes.installEdge,
                throwing: false,
            });
            if (result?.installed) {
                setInstalled(true);
                toast.success(t("microsoft.browserInstalled"));
                setTimeout(() => {
                    closeModal?.();
                    onInstalled?.();
                }, 1500);
            }
            else {
                const msg = result?.error ?? t("microsoft.chromiumInstallFailed");
                setError(msg);
                toast.error(t("microsoft.chromiumInstallFailed"), msg);
            }
        }
        catch (e) {
            const message = e instanceof Error ? e.message : String(e);
            setError(message);
            toast.error(t("microsoft.chromiumInstallFailed"), message);
        }
        finally {
            setInstalling(false);
        }
    };
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("microsoft.chromiumRequired"), strOKButtonText: installed
            ? t("microsoft.chromiumInstalled")
            : installing
                ? t("microsoft.chromiumInstalling")
                : t("microsoft.chromiumInstallButton"), strCancelButtonText: t("common.cancel"), onOK: installed ? closeModal : handleInstall, onCancel: closeModal, bOKDisabled: installing, bHideCloseIcon: installing, children: SP_JSX.jsxs("div", { style: { padding: "8px 0" }, children: [!installing && !installed && !error && (SP_JSX.jsx("p", { style: { fontSize: 14, lineHeight: 1.5 }, children: t("microsoft.chromiumRequiredMessage") })), installing && (SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        alignItems: "center",
                        gap: 12,
                        padding: "16px 0",
                    }, children: [SP_JSX.jsx(DFL.Spinner, { width: 24, height: 24 }), SP_JSX.jsx("span", { style: { fontSize: 14 }, children: t("microsoft.chromiumInstalling") })] })), installed && (SP_JSX.jsx("p", { style: { fontSize: 14, color: "#4ade80" }, children: t("microsoft.chromiumInstalled") })), error && SP_JSX.jsx("p", { style: { fontSize: 14, color: "#ef4444" }, children: error })] }) }));
};

/**
 * Backend auth-failure codes that mean "the network was down /
 * flaky", not a real credential problem. Surfaced with an
 * actionable "connect to Wi-Fi" message instead of the raw code.
 * `token_exchange_network_error` — code captured but the token
 * exchange failed after retries; `network_unreachable` — the
 * browser never reached the login page (offline fast-fail).
 */
const NETWORK_ERROR_CODES = new Set([
    "token_exchange_network_error",
    "network_unreachable",
]);
/**
 * Hook that drives per-store auth flows. Delegates the
 * full handshake to {@link AuthDispatcher} and surfaces
 * the result as toasts. Status is reactive : auth events
 * trigger a re-render in `AuthContext` so the UI flips
 * Connect → Connected without polling.
 *
 * @param storeId — id of the store to drive.
 * @returns auth state + action callbacks.
 */
function useStoreAuth(store) {
    const auth = useAuth();
    const { stores } = useStores();
    const toast = useToast();
    const { t } = useTranslation();
    const [busy, setBusy] = SP_REACT.useState(false);
    const info = stores.find((s) => s.name === store) ?? null;
    const status = auth.statuses[store];
    const connect = SP_REACT.useCallback(async () => {
        setBusy(true);
        try {
            toast.info(`Starting ${store} sign-in…`);
            const result = await AuthDispatcher.start(store);
            // Browser-based OAuth needs Microsoft Edge. When the
            // backend reports the prereq is missing, surface a
            // modal with an Install button rather than a useless
            // toast. The modal retries the auth flow on success.
            if (!result.success && result.error === "edge_not_installed") {
                DFL.showModal(SP_JSX.jsx(ChromiumInstallModal, { onInstalled: () => {
                        // Retry the auth flow now that Edge is in.
                        void connect();
                    }, closeModal: () => { } }));
                return result;
            }
            if (result.success) {
                auth.notifyConnected(store);
                toast.success(`${store} connected`);
            }
            else if (result.error && NETWORK_ERROR_CODES.has(result.error)) {
                toast.error(t("auth.errors.networkTitle"), t("auth.errors.networkBody"));
            }
            else {
                toast.error(`${store} sign-in failed`, result.error);
            }
            return result;
        }
        catch (e) {
            const message = e instanceof Error ? e.message : String(e);
            toast.error(`${store} sign-in failed`, message);
            return { success: false, store, error: message };
        }
        finally {
            setBusy(false);
        }
    }, [store, toast, auth, t]);
    const disconnect = SP_REACT.useCallback(async () => {
        setBusy(true);
        try {
            await auth.logout(store);
        }
        finally {
            setBusy(false);
        }
    }, [auth, store]);
    return { info, status, busy, connect, disconnect };
}

// THIS FILE IS AUTO GENERATED
function SiAmazongames (props) {
  return GenIcon({"attr":{"role":"img","viewBox":"0 0 24 24"},"child":[{"tag":"path","attr":{"d":"M3.8457 11.1425c.09-.303.082-.685.082-1.078V7.2513c0-.135.033-.52-.02-.6-.054-.08-.26-.062-.404-.062-.412 0-.595-.057-.632.336l-.016.01c-.323-.247-.63-.482-1.223-.47l-.197.015a1.875 1.875 0 0 0-.3361.082 1.622 1.622 0 0 0-.886.89 3.0171 3.0171 0 0 0-.171.607 1.967 1.967 0 0 0-.031.62c.03.195.012.365.05.535.145.609.43 1.083.955 1.31a1.665 1.665 0 0 0 .917.114 1.73 1.73 0 0 0 .59-.22l.223-.164h.005a1.857 1.857 0 0 1-.01.556c-.091.526-.246.704-.771.797a1.483 1.483 0 0 1-.446.011l-.435-.057-.638-.154a.14.14 0 0 0-.103.088 3.3121 3.3121 0 0 0 .02.6431c.073.122.31.17.457.218.494.157 1.292.233 1.828.047.613-.211 1-.624 1.192-1.261zm-1.09-3.0421v1.445a1.255 1.255 0 0 1-.356.176c-.872.274-1.2021-.34-1.2021-1.144a2.087 2.087 0 0 1 .093-.731c.077-.198.195-.371.39-.45.068-.03.152-.022.227-.053a1.745 1.745 0 0 1 .642.1l.193.09c.047.083.014.438.014.567zm3.509 2.756a2.114 2.114 0 0 0 .689-.265l.2741-.202c.07.092.043.231.134.301.092.07.798.096.865 0 .068-.095.037-.302.037-.446v-1.848c0-.36.027-.763-.057-1.046-.197-.667-.76-.896-1.632-.887l-.466.03a5.1102 5.1102 0 0 0-.788.172c-.112.039-.283.068-.33.17-.045.104-.036.647.026.69.093.063.536-.083.66-.11.357-.077 1.173-.156 1.372.1.144.184.12.544.116.876-.082.02-.224-.035-.31-.052l-.394-.041a1.483 1.483 0 0 0-.435.015c-.676.116-1.154.373-1.31 1-.243.963.592 1.746 1.549 1.544Zm-.197-1.786c.098-.058.22-.042.342-.09.276 0 .509.043.7401.053.043.093.013.563.01.705-.196.261-.988.521-1.257.15-.194-.275-.064-.684.165-.818Zm3.1752 1.318c0 .144-.018.287.072.342.05.03.172.016.247.016.173 0 .765.036.83-.047.082-.103.035-.645.035-.834v-2.261a1.594 1.594 0 0 1 .503-.187c.279-.065.648-.066.787.12.123.164.104.457.104.745v1.565c0 .213-.048.807.072.881.074.045.412.016.528.016.117 0 .457.028.53-.026.097-.074.056-.6.056-.788q0-1.164.005-2.331c.04-.065.202-.092.28-.12.3421-.12.8751-.2 1.0411.135.082.173.057.488.057.742v1.552c0 .185-.041.687.036.788.046.059.151.047.254.047.155 0 .77.027.84-.026.102-.078.061-.39.061-.565v-1.61c0-.68.053-1.298-.247-1.685-.537-.7-1.953-.325-2.472.12h-.02c-.199-.334-.54-.522-1.081-.514l-.218.016a2.49 2.49 0 0 0-.435.098 3.3441 3.3441 0 0 0-.5231.247l-.254.156c-.092-.111-.036-.292-.165-.368-.05-.03-.714-.038-.807-.015a.165.165 0 0 0-.088.057c-.055.082-.027.625-.027.782zm7.9362.175c.51.32 1.426.389 2.15.208.182-.045.5371-.11.6001-.259a3.9771 3.9771 0 0 0 .01-.622.15.15 0 0 0-.066-.077c-.065-.028-.165.02-.228.036l-.534.098a1.95 1.95 0 0 1-.59.022 3.2441 3.2441 0 0 1-.412-.058c-.406-.123-.675-.425-.68-.943.097-.043.413-.01.544-.01h1.32c.19 0 .493.04.61-.047.155-.116.144-.894.093-1.144-.17-.85-.682-1.318-1.71-1.3l-.124.005-.373.05c-.4.102-.746.287-.968.566a2.117 2.117 0 0 0-.3061.494c-.203.482-.259 1.353-.087 1.93.138.46.381.818.751 1.049zm.305-2.682c.093-.268.247-.463.518-.55l.232-.044c.615-.005.784.313.777.928-.083.038-.35.01-.467.01h-1.117c-.034-.086.03-.27.057-.347zm6.3982 1.087c-.247-.428-.784-.595-1.264-.792-.227-.094-.6-.172-.653-.435-.113-.564 1.045-.4 1.388-.316.096.023.317.11.378.03s.032-.27.032-.393c0-.101.013-.23-.027-.295-.059-.1-.215-.128-.336-.165a3.0161 3.0161 0 0 0-.964-.14l-.296.022a2.396 2.396 0 0 0-.538.144 1.223 1.223 0 0 0-.659.642c-.184.455-.01 1.058.265 1.28.33.266.777.417 1.2.59.215.088.458.138.504.394.122.697-1.196.444-1.59.342-.083-.022-.343-.11-.413-.072-.094.053-.057.317-.057.46 0 .083-.012.2.021.254.1.165.645.269.886.317.956.19 1.806-.134 2.113-.736.154-.291.182-.834.01-1.133zm-3.357 4.0871-.18.01a3.0931 3.0931 0 0 0-.5241.058 3.6181 3.6181 0 0 0-1.114.39c-.083.05-.412.211-.352.361.047.12.524.02.678.01.305-.02.454-.047.742-.046h.528a2.958 2.958 0 0 1 .472.03.702.702 0 0 1 .284.136c.12.103.098.452.057.616a11.4804 11.4804 0 0 1-.476 1.5541c-.034.082-.3.602-.005.54.133-.03.404-.33.487-.43.412-.528.672-1.134.865-1.896.045-.178.147-.874.046-1.036-.165-.269-1.037-.306-1.508-.297zm-.6431 1.483-.472.207-.78.2951c-.697.27-1.47.446-2.2451.638l-.907.17-1.067.14-.342.026c-.136.03-.297.012-.4501.036a6.8722 6.8722 0 0 1-1.02.042l-.772.005-.555-.02-.357-.02c-.082-.017-.18-.006-.27-.022l-1.0821-.119c-.32-.06-.665-.082-.97-.165l-.253-.042-.9411-.21c-.673-.21-1.341-.375-1.97-.617-.696-.27-1.4241-.564-2.0401-.928l-.61-.352c-.112-.066-.202-.164-.38-.164-.131.065-.171.15-.081.305a1.049 1.049 0 0 0 .238.233l.28.26c.404.32.81.65 1.248.937.38.247.783.457 1.18.69l.597.284c.34.15.694.305 1.05.445l.9181.305c.495.165 1.033.248 1.566.38l1.1241.163.362.027.612.046h.222a2.504 2.504 0 0 0 .653.006l.233-.006.285-.005.9741-.077c.437-.098.897-.083 1.31-.208l.954-.232a16.0985 16.0985 0 0 0 1.6731-.586 11.8764 11.8764 0 0 0 1.9471-1.036c.211-.142.51-.247.565-.544.047-.25-.21-.347-.427-.287z"},"child":[]}]})(props);
}function SiEpicgames (props) {
  return GenIcon({"attr":{"role":"img","viewBox":"0 0 24 24"},"child":[{"tag":"path","attr":{"d":"M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4.262 4.262 0 00.02.433c.031.3.037.59.316.92.027.033.311.245.311.245.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2 0 0 .284-.211.311-.243.28-.33.285-.621.316-.92a4.261 4.261 0 00.02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.708 1.708 0 01.591.108 1.745 1.745 0 01.49.299l-.452.546a1.247 1.247 0 00-.308-.195.91.91 0 00-.363-.068.658.658 0 00-.28.06.703.703 0 00-.224.163.783.783 0 00-.151.243.799.799 0 00-.056.299v.008a.852.852 0 00.056.31.7.7 0 00.157.245.736.736 0 00.238.16.774.774 0 00.303.058.79.79 0 00.445-.116v-.339h-.548v-.565H7.37v1.255a2.019 2.019 0 01-.524.307 1.789 1.789 0 01-.683.123 1.642 1.642 0 01-.602-.107 1.46 1.46 0 01-.478-.3 1.371 1.371 0 01-.318-.455 1.438 1.438 0 01-.115-.58v-.008a1.426 1.426 0 01.113-.57 1.449 1.449 0 01.312-.46 1.418 1.418 0 01.474-.309 1.58 1.58 0 01.598-.111 1.708 1.708 0 01.045 0zm11.963.008a2.006 2.006 0 01.612.094 1.61 1.61 0 01.507.277l-.386.546a1.562 1.562 0 00-.39-.205 1.178 1.178 0 00-.388-.07.347.347 0 00-.208.052.154.154 0 00-.07.127v.008a.158.158 0 00.022.084.198.198 0 00.076.066.831.831 0 00.147.06c.062.02.14.04.236.061a3.389 3.389 0 01.43.122 1.292 1.292 0 01.328.17.678.678 0 01.207.24.739.739 0 01.071.337v.008a.865.865 0 01-.081.382.82.82 0 01-.229.285 1.032 1.032 0 01-.353.18 1.606 1.606 0 01-.46.061 2.16 2.16 0 01-.71-.116 1.718 1.718 0 01-.593-.346l.43-.514c.277.223.578.335.9.335a.457.457 0 00.236-.05.157.157 0 00.082-.142v-.008a.15.15 0 00-.02-.077.204.204 0 00-.073-.066.753.753 0 00-.143-.062 2.45 2.45 0 00-.233-.062 5.036 5.036 0 01-.413-.113 1.26 1.26 0 01-.331-.16.72.72 0 01-.222-.243.73.73 0 01-.082-.36v-.008a.863.863 0 01.074-.359.794.794 0 01.214-.283 1.007 1.007 0 01.34-.185 1.423 1.423 0 01.448-.066 2.006 2.006 0 01.025 0zm-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z"},"child":[]}]})(props);
}function SiGogdotcom (props) {
  return GenIcon({"attr":{"role":"img","viewBox":"0 0 24 24"},"child":[{"tag":"path","attr":{"d":"M7.15 15.24H4.36a.4.4 0 0 0-.4.4v2c0 .21.18.4.4.4h2.8v1.32h-3.5c-.56 0-1.02-.46-1.02-1.03v-3.39c0-.56.46-1.02 1.03-1.02h3.48v1.32zM8.16 11.54c0 .58-.47 1.05-1.05 1.05H2.63v-1.35h3.78a.4.4 0 0 0 .4-.4V6.39a.4.4 0 0 0-.4-.4H4.39a.4.4 0 0 0-.41.4v2.02c0 .23.18.4.4.4H6v1.35H3.68c-.58 0-1.05-.46-1.05-1.04V5.68c0-.57.47-1.04 1.05-1.04H7.1c.58 0 1.05.47 1.05 1.04v5.86zM21.36 19.36h-1.32v-4.12h-.93a.4.4 0 0 0-.4.4v3.72h-1.33v-4.12h-.93a.4.4 0 0 0-.4.4v3.72h-1.33v-4.42c0-.56.46-1.02 1.03-1.02h5.61v5.44zM21.37 11.54c0 .58-.47 1.05-1.05 1.05h-4.48v-1.35h3.78a.4.4 0 0 0 .4-.4V6.39a.4.4 0 0 0-.4-.4h-2.03a.4.4 0 0 0-.4.4v2.02c0 .23.18.4.4.4h1.62v1.35H16.9c-.58 0-1.05-.46-1.05-1.04V5.68c0-.57.47-1.04 1.05-1.04h3.43c.58 0 1.05.47 1.05 1.04v5.86zM13.72 4.64h-3.44c-.58 0-1.04.47-1.04 1.04v3.44c0 .58.46 1.04 1.04 1.04h3.44c.57 0 1.04-.46 1.04-1.04V5.68c0-.57-.47-1.04-1.04-1.04m-.3 1.75v2.02a.4.4 0 0 1-.4.4h-2.03a.4.4 0 0 1-.4-.4V6.4c0-.22.17-.4.4-.4H13c.23 0 .4.18.4.4zM12.63 13.92H9.24c-.57 0-1.03.46-1.03 1.02v3.39c0 .57.46 1.03 1.03 1.03h3.39c.57 0 1.03-.46 1.03-1.03v-3.39c0-.56-.46-1.02-1.03-1.02m-.3 1.72v2a.4.4 0 0 1-.4.4v-.01H9.94a.4.4 0 0 1-.4-.4v-1.99c0-.22.18-.4.4-.4h2c.22 0 .4.18.4.4zM23.49 1.1a1.74 1.74 0 0 0-1.24-.52H1.75A1.74 1.74 0 0 0 0 2.33v19.34a1.74 1.74 0 0 0 1.75 1.75h20.5A1.74 1.74 0 0 0 24 21.67V2.33c0-.48-.2-.92-.51-1.24m0 20.58a1.23 1.23 0 0 1-1.24 1.24H1.75A1.23 1.23 0 0 1 .5 21.67V2.33a1.23 1.23 0 0 1 1.24-1.24h20.5a1.24 1.24 0 0 1 1.24 1.24v19.34z"},"child":[]}]})(props);
}function SiUbisoft (props) {
  return GenIcon({"attr":{"role":"img","viewBox":"0 0 24 24"},"child":[{"tag":"path","attr":{"d":"M23.561 11.988C23.301-.304 6.954-4.89.656 6.634c.282.206.661.477.943.672a11.747 11.747 0 00-.976 3.067 11.885 11.885 0 00-.184 2.071C.439 18.818 5.621 24 12.005 24c6.385 0 11.556-5.17 11.556-11.556v-.455zm-20.27 2.06c-.152 1.246-.054 1.636-.054 1.788l-.282.098c-.108-.206-.37-.932-.488-1.908C2.163 10.308 4.7 6.96 8.57 6.33c3.544-.52 6.937 1.68 7.728 4.758l-.282.098c-.087-.087-.228-.336-.77-.878-4.281-4.281-11.002-2.32-11.956 3.74zm11.002 2.081a3.145 3.145 0 01-2.59 1.355 3.15 3.15 0 01-3.155-3.155 3.159 3.159 0 012.927-3.144c1.018-.043 1.972.51 2.416 1.398a2.58 2.58 0 01-.455 2.95c.293.205.575.4.856.595zm6.58.12c-1.669 3.782-5.106 5.766-8.77 5.712-7.034-.347-9.083-8.466-4.38-11.393l.207.206c-.076.108-.358.325-.791 1.182-.51 1.041-.672 2.081-.607 2.732.369 5.67 8.314 6.83 11.045 1.214C21.057 8.217 11.822.401 3.626 6.374l-.184-.184C5.599 2.808 9.816 1.3 13.837 2.309c6.147 1.55 9.453 7.956 7.035 13.94z"},"child":[]}]})(props);
}

const STORE_ICONS = {
    steam: FaSteam,
    epic: SiEpicgames,
    gog: SiGogdotcom,
    amazon: SiAmazongames,
    microsoft: FaXbox,
    ubisoft: SiUbisoft,
};
const StoreIcon = ({ store, size = 16, color = "inherit", }) => {
    const Icon = STORE_ICONS[store] ?? FaGamepad;
    return SP_JSX.jsx(Icon, { size: size, color: color });
};

// THIS FILE IS AUTO GENERATED
function FiLogOut (props) {
  return GenIcon({"attr":{"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":"2","strokeLinecap":"round","strokeLinejoin":"round"},"child":[{"tag":"path","attr":{"d":"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"},"child":[]},{"tag":"polyline","attr":{"points":"16 17 21 12 16 7"},"child":[]},{"tag":"line","attr":{"x1":"21","y1":"12","x2":"9","y2":"12"},"child":[]}]})(props);
}

const StoreAuthButton = ({ store, status, onConnect, onDisconnect, busy, }) => {
    const { t } = useTranslation();
    if (status === "checking" || status === "error")
        return null;
    const isConnected = status === "connected";
    return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("style", { children: `
        .unifideck-store-auth-button.connected {
          background-color: #ef4444 !important;
          color: #fff;
        }
        .unifideck-store-auth-button.connected:focus,
        .unifideck-store-auth-button.connected:hover {
          color: #ef4444 !important;
          background-color: #fff !important;
        }
      ` }), SP_JSX.jsx(DFL.DialogButton, { disabled: busy, className: `unifideck-store-auth-button ${isConnected ? "connected" : "disconnected"}`, onClick: () => (isConnected ? onDisconnect(store) : onConnect(store)), style: {
                    display: "inline-flex",
                    alignItems: "center",
                    justifyContent: "center",
                    padding: "4px 10px",
                    fontSize: 10,
                    height: 28,
                    width: "fit-content",
                    minWidth: "unset",
                }, children: isConnected ? (SP_JSX.jsx(FiLogOut, { size: 12 })) : (t("storeConnections.authenticate")) })] }));
};

const ROW_CONFIG = {
    epic: {
        notInstalledStatus: "legendary_not_installed",
        notInstalledMessage: "storeConnections.legendaryNotInstalled",
    },
    amazon: {
        notInstalledStatus: "nile_not_installed",
        notInstalledMessage: "storeConnections.nileNotInstalled",
    },
};
const StoreRow = ({ storeId, displayName, }) => {
    const { t } = useTranslation();
    const { status, busy, connect, disconnect } = useStoreAuth(storeId);
    const isConnected = status === "connected";
    const config = ROW_CONFIG[storeId];
    const showNotInstalled = config?.notInstalledStatus && status === config.notInstalledStatus;
    return (SP_JSX.jsxs("div", { children: [SP_JSX.jsxs("div", { style: {
                    display: "flex",
                    alignItems: "center",
                    flexDirection: "row",
                    justifyContent: "space-between",
                    padding: 0,
                }, children: [SP_JSX.jsxs("div", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx(StoreIcon, { store: storeId, size: "18px", color: isConnected ? "#4ade80" : "#fff" }), SP_JSX.jsx("span", { style: { fontSize: 14 }, children: displayName })] }), SP_JSX.jsx(StoreAuthButton, { store: storeId, status: status ?? "disconnected", busy: busy, onConnect: () => void connect(), onDisconnect: () => void disconnect() })] }), showNotInstalled && config?.notInstalledMessage && (SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.Field, { description: t(config.notInstalledMessage) }) }))] }));
};
const StoreConnections = () => {
    const { t } = useTranslation();
    const { stores, loading } = useStores();
    if (loading)
        return null;
    return (SP_JSX.jsx(DFL.PanelSection, { title: t("storeConnections.title"), children: SP_JSX.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 2 }, children: stores.map((s) => (SP_JSX.jsx(StoreRow, { storeId: s.name, displayName: s.display_name }, s.name))) }) }));
};

/**
 * Two-button picker : "Resync everything (including artwork)"
 * vs "Resync library, keep artwork". The native ConfirmModal
 * footer is hidden so only the two stacked DialogButtons are
 * actionable.
 */
const ForceSyncModal = ({ onResyncArtwork, onKeepArtwork, closeModal, }) => {
    const { t } = useTranslation();
    return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("style", { children: `
        .unifideck-force-sync-modal + div { display: none !important; }
        .unifideck-force-sync-modal .DialogFooter { display: none !important; }
      ` }), SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("confirmModals.forceSyncTitle"), strDescription: "", bHideCloseIcon: false, onOK: closeModal, onCancel: closeModal, children: SP_JSX.jsxs("div", { className: "unifideck-force-sync-modal", style: { padding: "10px 0" }, children: [SP_JSX.jsx("div", { style: {
                                marginBottom: 20,
                                color: "#cbd5e1",
                                fontSize: 14,
                                lineHeight: 1.5,
                            }, children: t("confirmModals.forceSyncDescription") }), SP_JSX.jsxs("div", { style: {
                                display: "flex",
                                flexDirection: "column",
                                gap: 10,
                            }, children: [SP_JSX.jsxs(DFL.DialogButton, { onClick: () => {
                                        closeModal?.();
                                        onResyncArtwork();
                                    }, style: {
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: 8,
                                        color: "#ef4444",
                                    }, children: [SP_JSX.jsx(FaImage, {}), " ", t("confirmModals.resyncArtwork")] }), SP_JSX.jsxs(DFL.DialogButton, { onClick: () => {
                                        closeModal?.();
                                        onKeepArtwork();
                                    }, style: {
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: 8,
                                    }, children: [SP_JSX.jsx(FaSync, {}), " ", t("confirmModals.keepArtwork")] })] })] }) })] }));
};

// The progress block hides only on terminal statuses (sync not
// running). Allow-listing in-progress phases is fragile because
// the post-sync services (Metadata, Artwork, Compatibility) all
// overwrite ``progress.status`` independently — Compatibility
// sets ``"proton_meta"`` and leaves it there, so the bar would
// stay hidden during artwork download if we required a specific
// in-progress value. Inverting the check makes new phases work
// without needing to add them here.
const TERMINAL_STATUSES = new Set(["idle", "complete", "error", "cancelled"]);
const LibrarySync = () => {
    const { t } = useTranslation();
    const sync = useSync();
    const cooldown = useSyncCooldown();
    const isSyncing = sync.isSyncing;
    const isCancelling = sync.isCancelling;
    const progress = sync.progress;
    const onForceSync = () => {
        DFL.showModal(SP_JSX.jsx(ForceSyncModal, { onResyncArtwork: () => void sync.forceSync(true), onKeepArtwork: () => void sync.forceSync(false), closeModal: () => { } }));
    };
    const isArtwork = progress?.status === "artwork";
    const isComplete = progress?.status === "complete";
    const isError = progress?.status === "error";
    const barColor = isError
        ? "#ff6b6b"
        : isComplete
            ? "#4caf50"
            : isArtwork
                ? "#ff9800"
                : "#1a9fff";
    return (SP_JSX.jsxs(DFL.PanelSection, { title: t("librarySync.title"), children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", disabled: isSyncing || !cooldown.canSync, onClick: () => void sync.startSync(), children: SP_JSX.jsxs("div", { style: {
                            display: "flex",
                            alignItems: "center",
                            gap: 8,
                            justifyContent: "center",
                        }, children: [SP_JSX.jsx(FaSync, { style: {
                                    animation: isSyncing ? "spin 1s linear infinite" : "none",
                                    opacity: cooldown.canSync ? 1 : 0.5,
                                } }), isSyncing
                                ? t("librarySync.syncing")
                                : !cooldown.canSync
                                    ? `${cooldown.remainingSecs}s`
                                    : t("librarySync.syncLibraries")] }) }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", disabled: isSyncing || !cooldown.canSync, onClick: onForceSync, children: SP_JSX.jsxs("div", { style: {
                            display: "flex",
                            alignItems: "center",
                            gap: 8,
                            justifyContent: "center",
                        }, children: [SP_JSX.jsx(FaSync, { style: { opacity: cooldown.canSync ? 1 : 0.5 } }), isSyncing ? "…" : t("librarySync.forceSync")] }) }) }), isSyncing && (SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", onClick: () => void sync.cancelSync(), disabled: isCancelling, children: isCancelling
                        ? t("librarySync.cancelling", "Cancelling…")
                        : t("librarySync.cancelSync") }) })), progress && !TERMINAL_STATUSES.has(progress.status) && (SP_JSX.jsxs("div", { style: { fontSize: 12, width: "100%" }, children: [SP_JSX.jsx("div", { style: { marginBottom: 5, opacity: 0.9 }, children: progress.current_game?.label
                            ? t(progress.current_game.label, progress.current_game.values ?? {})
                            : "…" }), SP_JSX.jsx("div", { style: {
                            width: "100%",
                            height: 4,
                            backgroundColor: "#333",
                            borderRadius: 2,
                            overflow: "hidden",
                        }, children: SP_JSX.jsx("div", { style: {
                                width: `${progress.progress_percent ?? 0}%`,
                                height: "100%",
                                backgroundColor: barColor,
                                transition: "width 0.3s ease",
                            } }) }), SP_JSX.jsxs("div", { style: { marginTop: 5, opacity: 0.7 }, children: [(progress.total_games ?? 0) > 0 && (SP_JSX.jsx("div", { children: t("librarySync.gamesSynced", {
                                    synced: progress.synced_games ?? 0,
                                    total: progress.total_games ?? 0,
                                }) })), (progress.artwork_total ?? 0) > 0 && (SP_JSX.jsx("div", { children: t("librarySync.artworkDownloaded", {
                                    synced: progress.artwork_synced ?? 0,
                                    total: progress.artwork_total ?? 0,
                                }) })), (progress.steam_total ?? 0) > 0 && (SP_JSX.jsx("div", { children: t("librarySync.steamMetadataDownloaded", {
                                    synced: progress.steam_synced ?? 0,
                                    total: progress.steam_total ?? 0,
                                }) })), (progress.unifidb_total ?? 0) > 0 && (SP_JSX.jsx("div", { children: t("librarySync.unifidbMetadataDownloaded", {
                                    synced: progress.unifidb_synced ?? 0,
                                    total: progress.unifidb_total ?? 0,
                                }) })), (progress.metacritic_total ?? 0) > 0 && (SP_JSX.jsx("div", { children: t("librarySync.metacriticMetadataDownloaded", {
                                    synced: progress.metacritic_synced ?? 0,
                                    total: progress.metacritic_total ?? 0,
                                }) })), (progress.compat_total ?? 0) > 0 && (SP_JSX.jsx("div", { children: t("librarySync.compatChecked", {
                                    synced: progress.compat_synced ?? 0,
                                    total: progress.compat_total ?? 0,
                                }) }))] }), progress.error && (SP_JSX.jsxs("div", { style: { color: "#ff6b6b", marginTop: 5 }, children: [t("librarySync.error"), ": ", progress.error] }))] }))] }));
};

const AUTO_DETECT = "auto";
/**
 * Locale picker — lists every locale shipped under
 * `src/i18n/locales/`, sourced from the generated module so the
 * UI never drift from the DeepL translation pipeline.
 */
const LanguageSelector = () => {
    const { t } = useTranslation();
    const { locale, setLocale } = useLocale();
    // Memoise the option list — `SUPPORTED_LANGUAGES` is a
    // module-level constant, so this only runs once. The translation
    // of "Auto-detect" can change at runtime when the user switches
    // the active UI language, so we depend on `t` to re-derive it.
    const options = SP_REACT.useMemo(() => [
        {
            data: AUTO_DETECT,
            label: t("settings.languageAuto", { defaultValue: "Auto-detect" }),
        },
        ...SUPPORTED_LANGUAGES.map((tag) => ({
            data: tag,
            // Native-script name. Fall back to the BCP 47 tag itself
            // if a name is missing — defensive : LANGUAGE_NAMES is
            // generated from the same source so this branch should be
            // unreachable in practice.
            label: LANGUAGE_NAMES$1[tag] ?? tag,
        })),
    ], [t]);
    return (SP_JSX.jsx(DFL.PanelSection, { title: t("settings.language"), children: SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.Dropdown, { rgOptions: options, selectedOption: locale, onChange: (opt) => void setLocale(String(opt.data)) }) }) }));
};

/**
 * useViewMode — game-detail view-mode preference.
 *
 * Persists to localStorage (survives Steam restarts) with a
 * window-property cache for fast in-session access. Broadcasts
 * changes via a custom event so every mounted consumer stays in
 * sync without prop drilling.
 */

const LS_KEY = "unifideck:view-mode";
const WIN_KEY = "__unifideck_view_mode__";
const CHANGE_EVENT = "unifideck:view-mode-change";
const DEFAULT_MODE = "compact";
function readMode() {
    // window cache first (fast path within a session)
    const w = window;
    const cached = w[WIN_KEY];
    if (cached === "compact" || cached === "full")
        return cached;
    // fall back to localStorage (survives restarts)
    try {
        const stored = localStorage.getItem(LS_KEY);
        if (stored === "compact" || stored === "full")
            return stored;
    }
    catch {
        /* CEF may deny access */
    }
    return DEFAULT_MODE;
}
function writeMode(mode) {
    window[WIN_KEY] = mode;
    try {
        localStorage.setItem(LS_KEY, mode);
    }
    catch {
        /* ignore */
    }
    window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: mode }));
}
function useViewMode() {
    const [mode, setModeState] = SP_REACT.useState(readMode);
    SP_REACT.useEffect(() => {
        const handler = (e) => {
            const detail = e.detail;
            if (detail)
                setModeState(detail);
        };
        window.addEventListener(CHANGE_EVENT, handler);
        return () => window.removeEventListener(CHANGE_EVENT, handler);
    }, []);
    const setMode = SP_REACT.useCallback((next) => {
        writeMode(next);
        setModeState(next);
    }, []);
    const toggle = SP_REACT.useCallback(() => {
        setMode(mode === "compact" ? "full" : "compact");
    }, [mode, setMode]);
    return { mode, setMode, toggle };
}

const GameDetailsViewModeToggle = () => {
    const { t } = useTranslation();
    const { mode, setMode } = useViewMode();
    const compact = mode === "compact";
    return (SP_JSX.jsx(DFL.PanelSection, { title: t("gameDetailsSettings.title"), children: SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: compact
                    ? t("gameDetailsSettings.simple")
                    : t("gameDetailsSettings.detailed"), checked: compact, onChange: (c) => setMode(c ? "compact" : "full") }) }) }));
};

/**
 * useCollectionsEnabled — opt-in for `[Unifideck]` Steam Collections.
 *
 * Thin React wrapper over the single source of truth in
 * `collection-manager` (localStorage + a broadcast event). The running
 * collection manager listens to the same event, so flipping this toggle
 * creates / deletes the collections live without a Steam restart.
 */

function useCollectionsEnabled() {
    const [enabled, setEnabledState] = SP_REACT.useState(isCollectionsEnabled);
    SP_REACT.useEffect(() => {
        const handler = (e) => {
            const detail = e.detail;
            if (typeof detail === "boolean")
                setEnabledState(detail);
        };
        window.addEventListener(COLLECTIONS_ENABLED_EVENT, handler);
        return () => window.removeEventListener(COLLECTIONS_ENABLED_EVENT, handler);
    }, []);
    const setEnabled = SP_REACT.useCallback((next) => {
        setCollectionsEnabled(next);
        setEnabledState(next);
    }, []);
    return { enabled, setEnabled };
}

const CollectionsToggle = () => {
    const { t } = useTranslation();
    const { enabled, setEnabled } = useCollectionsEnabled();
    return (SP_JSX.jsx(DFL.PanelSection, { title: t("collectionSettings.title"), children: SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: enabled
                    ? t("collectionSettings.enabled")
                    : t("collectionSettings.disabled"), checked: enabled, onChange: setEnabled }) }) }));
};

const CleanupConfirmModal = ({ onConfirm, closeModal }) => {
    const { t } = useTranslation();
    const [deleteFiles, setDeleteFiles] = SP_REACT.useState(false);
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("cleanup.modalTitle"), strDescription: t("cleanup.modalDescription"), strOKButtonText: t("cleanup.confirmDelete"), strCancelButtonText: t("cleanup.cancel"), onOK: () => {
            closeModal?.();
            onConfirm(deleteFiles);
        }, onCancel: closeModal, bHideCloseIcon: false, bDestructiveWarning: true, children: SP_JSX.jsx("div", { style: {
                padding: 12,
                background: "rgba(239, 68, 68, 0.08)",
                border: "1px solid rgba(239, 68, 68, 0.3)",
                borderRadius: 8,
                marginTop: 8,
            }, children: SP_JSX.jsx(DFL.ToggleField, { label: t("cleanup.deleteFilesLabel"), description: t("cleanup.deleteFilesDescription"), checked: deleteFiles, onChange: setDeleteFiles }) }) }));
};

const CleanupSection = () => {
    const { t } = useTranslation();
    const toast = useToast();
    const { mutate, loading, error } = useRPCMutation(rpcRoutes.performFullCleanup);
    const runCleanup = async (deleteFiles) => {
        // Feedback on confirm — the backend wipe can take several seconds.
        // Deferred a beat on purpose: ConfirmModal fires onOK → onConfirm
        // synchronously *during* its close animation, and Decky's toaster
        // swallows toasts raised mid-teardown (the later success toast only
        // survives because it fires after the await). Letting the modal fully
        // unmount first makes the "started" toast actually appear.
        setTimeout(() => toast.info(t("toasts.cleanupStarted"), t("toasts.cleanupStartedMessage")), 400);
        const result = await mutate(deleteFiles);
        if (!result) {
            toast.error(t("toasts.deleteFailed"), error?.message ?? t("errors.unknown"));
            return;
        }
        if (result.deleted_app_ids?.length) {
            const apps = window.SteamClient?.Apps;
            for (const id of result.deleted_app_ids) {
                try {
                    apps?.RemoveShortcut(id);
                }
                catch {
                    /* best effort */
                }
            }
        }
        await deleteAllUnifideckCollections().catch((e) => console.error("[Cleanup] delete collections failed", e));
        const totalTouched = result.deleted_games +
            result.deleted_artwork_count +
            result.logged_out_count +
            result.deleted_stray_files_count +
            result.deleted_files_count +
            (result.deleted_residual_count ?? 0);
        if (totalTouched === 0) {
            toast.info(t("toasts.cleanupNoop"), t("toasts.cleanupNoopMessage"));
        }
        else {
            toast.success(t("toasts.cleanupSuccessful"), t("toasts.cleanupSuccessfulMessage", {
                games: result.deleted_games,
                artwork: result.deleted_artwork_count,
                stores: result.logged_out_count,
                files: result.deleted_files_count,
                residual: result.deleted_residual_count ?? 0,
            }));
        }
    };
    const handleClick = () => {
        DFL.showModal(SP_JSX.jsx(CleanupConfirmModal, { onConfirm: runCleanup }));
    };
    return (SP_JSX.jsx(DFL.PanelSection, { title: t("cleanup.title"), children: SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", onClick: handleClick, disabled: loading, children: loading ? t("cleanup.deleting") : t("cleanup.deleteAll") }) }) }));
};

/**
 * Pure formatting helpers used across the downloads UI.
 *
 * Both functions are total: they return a non-empty string
 * for every numeric input including 0 and negatives, so
 * callers don't need defensive null/empty handling around
 * them.
 */
/** Format a byte count to a human-readable size with up to
 *  two fractional digits. `1024` → "1 KB", `1500` → "1.46 KB". */
function formatBytes$3(bytes) {
    if (bytes === 0)
        return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB", "TB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}

/**
 * Clipboard helper for the Steam CEF runtime.
 *
 * Steam's own UI always writes text through the *owning window's*
 * navigator (`<window>.navigator.clipboard.writeText(...)`), never the
 * ambient `navigator` — its Remote Play invite link, controller mapping
 * export and bug-report copy all do this. The reason matters here:
 * Gaming Mode renders the Quick Access panel into its own popup window,
 * and Chromium rejects `writeText` with `NotAllowedError` when the
 * document performing the write is not the focused one. Callers pass the
 * window that owns the element they were activated from.
 *
 * Lives in `utils/` rather than inside the component because components
 * must not reach for browser globals directly.
 *
 * Returns `false` instead of throwing — a failed copy is a UI nicety,
 * and the caller decides whether it is worth a toast.
 */
async function copyTextToClipboard(text, win) {
    const nav = (win ?? window).navigator;
    if (typeof nav?.clipboard?.writeText !== "function")
        return false;
    try {
        await nav.clipboard.writeText(text);
        return true;
    }
    catch (e) {
        console.warn("[Clipboard] writeText failed", e);
        return false;
    }
}

const CaptureLogsConfirmModal = ({ onConfirm, closeModal, }) => {
    const { t } = useTranslation();
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("captureLogs.modalTitle"), strDescription: t("captureLogs.modalDescription"), strOKButtonText: t("captureLogs.modalConfirm"), strCancelButtonText: t("captureLogs.cancel"), onOK: () => {
            closeModal?.();
            onConfirm();
        }, onCancel: closeModal, bHideCloseIcon: false, children: SP_JSX.jsxs("div", { style: {
                padding: 12,
                background: "rgba(59, 130, 246, 0.08)",
                border: "1px solid rgba(59, 130, 246, 0.3)",
                borderRadius: 8,
                marginTop: 8,
                display: "flex",
                flexDirection: "column",
                gap: 6,
                fontSize: 13,
            }, children: [SP_JSX.jsx("span", { children: t("captureLogs.modalIncludes") }), SP_JSX.jsx("span", { children: t("captureLogs.modalExcludes") })] }) }));
};

/** Last successful capture, kept across QAM dismount/remount. */
let lastCapture = null;
const CaptureLogsSection = () => {
    const { t } = useTranslation();
    const toast = useToast();
    const captureLogs = useRPC(rpcRoutes.captureLogs);
    const [result, setResult] = SP_REACT.useState(lastCapture);
    const [busy, setBusy] = SP_REACT.useState(false);
    // Authoritative re-entrancy latch. `busy` lags a render behind, and
    // `showModal` does not dedupe, so two stacked confirms could both
    // resolve before the first render commits — which would race two
    // captures on the same output filename.
    const runningRef = SP_REACT.useRef(false);
    // Gives us the window that owns this panel. In Gaming Mode that is a
    // separate popup window, and a clipboard write has to target it.
    const rowRef = SP_REACT.useRef(null);
    /**
     * Uses the low-level `useRPC` rather than `useRPCMutation` on purpose:
     * the mutation hook exposes its error via state read from the render
     * closure that created the handler, so on the *first* failure it is
     * still null and the message degrades to "unknown error". Catching the
     * thrown RpcError surfaces the backend's actual reason, which for the
     * one user-actionable failure here (nowhere writable to put the zip)
     * is the whole point of the toast.
     */
    const runCapture = async () => {
        if (runningRef.current)
            return;
        runningRef.current = true;
        setBusy(true);
        // Deferred a beat: ConfirmModal fires onOK synchronously during its
        // close animation, and Decky's toaster swallows toasts raised
        // mid-teardown. Same workaround as CleanupSection.
        setTimeout(() => toast.info(t("toasts.captureLogsStarted"), t("toasts.captureLogsStartedMessage")), 400);
        try {
            const captured = await captureLogs();
            if (captured.in_progress) {
                toast.info(t("toasts.captureLogsBusy"));
                return;
            }
            lastCapture = captured;
            setResult(captured);
            const failures = captured.errors?.length ?? 0;
            const vars = {
                path: captured.archive_path,
                size: formatBytes$3(captured.bytes),
                files: captured.file_count,
                failures,
            };
            if (failures > 0) {
                toast.show(t("toasts.captureLogsPartial"), t("toasts.captureLogsPartialMessage", vars), { duration: 10000 });
            }
            else {
                toast.success(t("toasts.captureLogsDone"), t("toasts.captureLogsDoneMessage", vars));
            }
        }
        catch (e) {
            toast.error(t("toasts.captureLogsFailed"), e instanceof Error ? e.message : t("errors.unknown"));
        }
        finally {
            runningRef.current = false;
            setBusy(false);
        }
    };
    const handleClick = () => {
        if (runningRef.current)
            return;
        DFL.showModal(SP_JSX.jsx(CaptureLogsConfirmModal, { onConfirm: () => void runCapture() }));
    };
    const handleCopy = async () => {
        if (!result)
            return;
        const ok = await copyTextToClipboard(result.archive_path, rowRef.current?.ownerDocument?.defaultView ?? null);
        if (ok)
            toast.info(t("toasts.captureLogsCopied"));
        // A failed copy still shows the path, so the user can read it off.
        else
            toast.error(t("toasts.captureLogsCopyFailed"), result.archive_path);
    };
    const failureCount = result?.errors?.length ?? 0;
    return (SP_JSX.jsxs(DFL.PanelSection, { title: t("captureLogs.title"), children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", onClick: handleClick, disabled: busy, description: t("captureLogs.description"), children: SP_JSX.jsxs("div", { style: {
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            gap: 8,
                        }, children: [busy && SP_JSX.jsx(DFL.Spinner, { width: 16, height: 16 }), busy ? t("captureLogs.capturing") : t("captureLogs.capture")] }) }) }), result && !busy && (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.Field, { ref: rowRef, label: t("captureLogs.savedTo"), description: 
                            // dir="ltr" keeps path segments in order under an RTL
                            // locale; break-all wraps rather than truncating,
                            // because a path the user has to find is worth three
                            // lines and useless with an ellipsis in it.
                            SP_JSX.jsx("span", { dir: "ltr", style: {
                                    fontSize: 12,
                                    lineHeight: "1.4",
                                    wordBreak: "break-all",
                                }, children: result.archive_path }), childrenContainerWidth: "fixed", bottomSeparator: "none" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.Field, { description: failureCount > 0
                                ? t("captureLogs.partialNote", { count: failureCount })
                                : t("captureLogs.bundleInfo", {
                                    files: result.file_count,
                                    size: formatBytes$3(result.bytes),
                                }), bottomSeparator: "none" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", onClick: () => void handleCopy(), children: t("captureLogs.copyPath") }) })] }))] }));
};

const parseInline = (text) => {
    const boldParts = text.split("**");
    const result = [];
    boldParts.forEach((part, boldIdx) => {
        const isBold = boldIdx % 2 !== 0;
        // Split by backticks for inline code styling
        const codeParts = part.split("`");
        const parsedCodeParts = codeParts.map((subPart, codeIdx) => {
            const isCode = codeIdx % 2 !== 0;
            if (isCode) {
                return (SP_JSX.jsx("code", { style: {
                        background: "rgba(255, 255, 255, 0.12)",
                        padding: "2px 6px",
                        borderRadius: "4px",
                        fontFamily: "monospace",
                        fontSize: "0.9em",
                        color: "#f59e0b",
                    }, children: subPart }, `${boldIdx}-${codeIdx}`));
            }
            return subPart;
        });
        if (isBold) {
            result.push(SP_JSX.jsx("strong", { style: {
                    color: "#38bdf8",
                    fontWeight: "bold",
                }, children: parsedCodeParts }, boldIdx));
        }
        else {
            result.push(...parsedCodeParts);
        }
    });
    return result;
};
const parseMarkdown = (text, t) => {
    if (!text) {
        return (SP_JSX.jsx("div", { style: { opacity: 0.6, fontStyle: "italic", padding: "10px 0" }, children: t("updater.noReleaseNotes", {
                defaultValue: "No release notes available.",
            }) }));
    }
    const lines = text.split("\n");
    return lines.map((line, idx) => {
        const trimmed = line.trim();
        // Headers
        if (trimmed.startsWith("### ")) {
            return (SP_JSX.jsx("h3", { style: {
                    margin: "12px 0 6px 0",
                    fontSize: "14px",
                    fontWeight: "bold",
                    color: "#ffffff",
                }, children: parseInline(trimmed.slice(4)) }, idx));
        }
        if (trimmed.startsWith("## ")) {
            return (SP_JSX.jsx("h2", { style: {
                    margin: "16px 0 8px 0",
                    fontSize: "16px",
                    fontWeight: "bold",
                    color: "#ffffff",
                    borderBottom: "1px solid rgba(255,255,255,0.1)",
                    paddingBottom: "4px",
                }, children: parseInline(trimmed.slice(3)) }, idx));
        }
        if (trimmed.startsWith("# ")) {
            return (SP_JSX.jsx("h1", { style: {
                    margin: "18px 0 10px 0",
                    fontSize: "18px",
                    fontWeight: "bold",
                    color: "#ffffff",
                    borderBottom: "1px solid rgba(255,255,255,0.15)",
                    paddingBottom: "6px",
                }, children: parseInline(trimmed.slice(2)) }, idx));
        }
        // Lists
        if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
            return (SP_JSX.jsx("li", { style: {
                    marginInlineStart: "20px",
                    marginBottom: "6px",
                    listStyleType: "disc",
                    lineHeight: "1.4",
                }, children: parseInline(trimmed.slice(2)) }, idx));
        }
        // Empty space/paragraph division
        if (trimmed === "") {
            return SP_JSX.jsx("div", { style: { height: "6px" } }, idx);
        }
        // Paragraph
        return (SP_JSX.jsx("p", { style: {
                margin: "4px 0",
                lineHeight: "1.4",
            }, children: parseInline(line) }, idx));
    });
};
const ReleaseNotesModal = ({ version, body, closeModal }) => {
    const { t } = useTranslation();
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("updater.modalTitle", {
            version,
            defaultValue: `UNIFIDECK v${version} — Release Notes`,
        }), strOKButtonText: t("common.ok", { defaultValue: "OK" }), onOK: closeModal, onCancel: closeModal, bHideCloseIcon: false, children: SP_JSX.jsx("div", { style: {
                maxHeight: "320px",
                overflowY: "auto",
                padding: "12px 16px",
                background: "rgba(0, 0, 0, 0.25)",
                borderRadius: "6px",
                marginTop: "12px",
                fontSize: "13px",
                color: "#e5e7eb",
                fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
            }, children: parseMarkdown(body, t) }) }));
};

// Decky PluginInstallType (backend enums.py / browser.py PluginInstallType)
const INSTALL_TYPE_REINSTALL = 1;
const INSTALL_TYPE_UPDATE = 2;
const INSTALL_TYPE_DOWNGRADE = 3;
// window.DeckyBackend lives on whichever window actually created this
// document. In Desktop Mode's full-page Decky Settings route, that's
// this window directly. In Gaming Mode, this panel renders inside the
// Quick Access Menu's own popup window (opened via window.open by
// Big Picture Mode) — DeckyBackend is undefined on that popup's own
// `window`, but reachable via `window.opener`. Falling back silently
// means every install/update button appears to do nothing in Gaming
// Mode's QAM while working fine in Desktop Mode, which is how this
// went unnoticed.
const getDeckyBackend = () => window.DeckyBackend ?? window.opener?.DeckyBackend ?? null;
// If Decky's own loader install dies silently (e.g. a 404 on a rotated
// dev-build asset — confirmed in journalctl: the browser CRITICAL "Could
// not fetch from URL" is followed by zero further progress/finish events),
// downloadActive would otherwise stay true forever. This is an inactivity
// timeout reset on every progress tick, not a single fixed deadline, so a
// legitimately slow ~40-50MB download over Wi-Fi isn't falsely flagged.
const INSTALL_WATCHDOG_TIMEOUT_MS = 45000;
// Parses the maintainer's dev-build filename convention (e.g.
// "unifideck.dev.0.7.1.g3f9a1c2.zip", or the legacy
// "unifideck.dev.v524.zip") into a display-friendly build id. Returns
// null when asset_name is absent or doesn't match — callers fall back
// to the generic "vDev" label in that case (e.g. a release built
// before this feature shipped, or a malformed manual upload).
const DEV_ASSET_NAME_RE = /^unifideck\.dev\.(.+)\.zip$/i;
const extractDevBuildId = (assetName) => {
    if (!assetName)
        return null;
    const m = assetName.match(DEV_ASSET_NAME_RE);
    return m ? m[1] : null;
};
const compareVersions = (a, b) => {
    const parse = (v) => v.split(".").map((x) => parseInt(x, 10) || 0);
    const pa = parse(a);
    const pb = parse(b);
    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
        const na = pa[i] || 0;
        const nb = pb[i] || 0;
        if (na > nb)
            return 1;
        if (na < nb)
            return -1;
    }
    return 0;
};
// Single source of truth for "is installing this release an update,
// downgrade, or reinstall relative to what's currently running" — used
// by both the install button's label and the actual install trigger,
// which used to each compute this independently and could disagree.
//
// Dev/prerelease releases are handled separately from stable ones:
// dev builds are deliberately cut BEFORE package.json's version bumps
// (see build-plugin.sh), so their parsed `version` is always the
// literal string "Dev" — running that through the numeric
// compareVersions() above always parses to 0, which is <= any real
// release and would misreport EVERY dev install as a "downgrade"
// regardless of how new the underlying code actually is. There is no
// meaningful downgrade concept for a prerelease: it's a "Reinstall"
// only when its build id matches what's already running (currentBuildId),
// and an "Update" otherwise.
const resolveInstallAction = (release, currentVersion, currentBuildId) => {
    if (release.prerelease) {
        const devBuildId = extractDevBuildId(release.asset_name);
        return {
            installType: devBuildId !== null && devBuildId === currentBuildId
                ? INSTALL_TYPE_REINSTALL
                : INSTALL_TYPE_UPDATE,
            displayVersion: devBuildId ?? release.version,
        };
    }
    const cmp = compareVersions(release.version, currentVersion);
    return {
        installType: cmp === 0
            ? INSTALL_TYPE_REINSTALL
            : cmp < 0
                ? INSTALL_TYPE_DOWNGRADE
                : INSTALL_TYPE_UPDATE,
        displayVersion: release.version,
    };
};
// Map Decky's download_progress_info.* keys to short, human status text.
const stageLabel = (key, t) => {
    const suffix = (key ?? "").split(".").pop() ?? "";
    switch (suffix) {
        case "start":
            return t("updater.stageStart", { defaultValue: "Starting…" });
        case "download_zip":
        case "increment_count":
            return t("updater.stageDownload", { defaultValue: "Downloading…" });
        case "open_zip":
            return t("updater.stageOpen", { defaultValue: "Reading package…" });
        case "parse_zip":
            return t("updater.stageParse", { defaultValue: "Verifying…" });
        case "uninstalling_previous":
            return t("updater.stageRemove", {
                defaultValue: "Removing old version…",
            });
        case "installing_plugin":
            return t("updater.stageInstall", { defaultValue: "Installing…" });
        case "download_remote":
            return t("updater.stageFinish", { defaultValue: "Finishing…" });
        default:
            return t("updater.stageWorking", { defaultValue: "Installing…" });
    }
};
// Best-effort lifecycle logging into the Unifideck log dir (per-session).
// The backend may be mid-reload near the end of an install, so failures are ignored.
const logEvent = (stage, detail) => {
    void call(rpcRoutes.logUpdateEvent, stage, detail).catch(() => { });
};
// Selected release tag, persisted across QAM mount/unmount — the Quick-Access
// panel dismounts when closed (and when the Dropdown overlay opens), which would
// otherwise reset the selection back to the default. Mirrors `persistentActiveTab`
// in QuickAccessPanel.tsx. Cleared naturally when the plugin reloads after install.
let persistentSelectedTag = null;
const PluginUpdater = () => {
    const { t } = useTranslation();
    const toast = useToast();
    // The selected release is identified by its unique tag (not the parsed
    // version, which could collide between a stable and a prerelease).
    const [selectedTag, setSelectedTag] = SP_REACT.useState("");
    const [installing, setInstalling] = SP_REACT.useState(false);
    const [checking, setChecking] = SP_REACT.useState(false);
    // Live install progress mirrored from Decky's loader events.
    const [downloadActive, setDownloadActive] = SP_REACT.useState(false);
    const [downloadPercent, setDownloadPercent] = SP_REACT.useState(0);
    const [downloadStatus, setDownloadStatus] = SP_REACT.useState("");
    const downloadActiveRef = SP_REACT.useRef(false);
    const watchdogTimerRef = SP_REACT.useRef(null);
    // Fetch updates status
    const { data: updateData, loading: checkingOnMount, refetch: checkUpdate, } = useRPCQuery(rpcRoutes.checkPluginUpdate, []);
    // Fetch available versions
    const { data: versionsData, loading: loadingVersions, refetch: refetchVersions, } = useRPCQuery(rpcRoutes.getAvailableVersions, []);
    // Cache-bypassing variants — used by the explicit "Check for Updates"
    // action and before installing a prerelease, whose single GitHub asset
    // gets deleted and re-uploaded under a new name/URL on every dev build.
    // The plain queries above stay on the 1-hour cache (mount-time auto-check,
    // background poller) so we don't hammer GitHub's unauthenticated rate limit.
    const forceCheckMut = useRPCMutation(rpcRoutes.forceCheckPluginUpdate);
    const forceVersionsMut = useRPCMutation(rpcRoutes.forceGetAvailableVersions);
    const currentVersion = updateData?.current ?? "0.0.0";
    const currentBuildId = updateData?.current_build_id ?? null;
    const initializedRef = SP_REACT.useRef(false);
    // Seed the selection when data loads. Restore a prior selection (persisted
    // across QAM remounts) if it still resolves to an available release; otherwise
    // default to the installed version, then latest, then newest.
    SP_REACT.useEffect(() => {
        if (updateData && versionsData && !initializedRef.current) {
            if (persistentSelectedTag &&
                versionsData.some((v) => v.tag === persistentSelectedTag)) {
                setSelectedTag(persistentSelectedTag);
            }
            else {
                // A prerelease row's `version` is always the literal string
                // "Dev" (no semver can be parsed from that tag), so it can
                // never equal currentVersion — match it via the build id
                // baked into its asset filename instead.
                //
                // currentBuildId !== null is also the deciding factor for the
                // stable-release branch below: dev builds are deliberately cut
                // BEFORE package.json's version gets bumped for release, so a
                // genuinely-installed dev build's `current` is identical to the
                // officially-tagged stable release sharing that same frozen
                // base version (e.g. both read "0.7.0"). Without this guard,
                // a real dev install would match the stable release's `version
                // === currentVersion` check too, seeding the selection (and
                // "(installed)" tag below) onto the wrong row.
                const current = versionsData.find((v) => v.prerelease
                    ? currentBuildId !== null &&
                        extractDevBuildId(v.asset_name) === currentBuildId
                    : currentBuildId === null && v.version === currentVersion);
                const tag = current?.tag ?? updateData.latest?.tag ?? versionsData[0]?.tag ?? "";
                persistentSelectedTag = tag || null;
                setSelectedTag(tag);
            }
            initializedRef.current = true;
        }
    }, [updateData, versionsData, currentVersion, currentBuildId]);
    // Subscribe to Decky's loader install events to mirror progress in-panel.
    SP_REACT.useEffect(() => {
        const backend = getDeckyBackend();
        if (!backend)
            return;
        const clearWatchdog = () => {
            if (watchdogTimerRef.current !== null) {
                clearTimeout(watchdogTimerRef.current);
                watchdogTimerRef.current = null;
            }
        };
        // Decky's own install code can die silently (confirmed: a 404 on a
        // rotated dev-build asset logs one CRITICAL line in journalctl and then
        // never fires plugin_download_info/finish again) — without this, the
        // panel freezes forever with downloadActive stuck true.
        const armWatchdog = () => {
            clearWatchdog();
            watchdogTimerRef.current = setTimeout(() => {
                if (!downloadActiveRef.current)
                    return;
                downloadActiveRef.current = false;
                setDownloadActive(false);
                logEvent("error", "watchdog_timeout after 45s of inactivity");
                toast.error(t("updater.installFailedTitle", { defaultValue: "Install Failed" }), t("updater.installTimeoutMessage", {
                    defaultValue: "No response from Decky Loader — the install may have stalled or failed. Please try again.",
                }));
            }, INSTALL_WATCHDOG_TIMEOUT_MS);
        };
        const onStart = (name) => {
            if (name !== "Unifideck")
                return;
            downloadActiveRef.current = true;
            setDownloadActive(true);
            setDownloadPercent(0);
            setDownloadStatus(stageLabel("start", t));
            logEvent("download_start", name);
            armWatchdog();
        };
        const onInfo = (percent, key) => {
            if (!downloadActiveRef.current)
                return;
            setDownloadPercent(percent);
            setDownloadStatus(stageLabel(key, t));
            logEvent("progress", `${percent}% ${key ?? ""}`.trim());
            armWatchdog();
        };
        const onFinish = (name) => {
            if (name !== "Unifideck")
                return;
            downloadActiveRef.current = false;
            setDownloadPercent(100);
            setDownloadActive(false);
            logEvent("download_finish", name);
            clearWatchdog();
        };
        backend.addEventListener("loader/plugin_download_start", onStart);
        backend.addEventListener("loader/plugin_download_info", onInfo);
        backend.addEventListener("loader/plugin_download_finish", onFinish);
        return () => {
            backend.removeEventListener("loader/plugin_download_start", onStart);
            backend.removeEventListener("loader/plugin_download_info", onInfo);
            backend.removeEventListener("loader/plugin_download_finish", onFinish);
            // Avoid a false-positive error toast firing after the QAM panel
            // unmounts while a download legitimately continues server-side.
            clearWatchdog();
        };
        // toast.error is useCallback-memoized in useToast(), so this is stable
        // and won't cause the effect (and its addEventListener subscriptions)
        // to re-run on every render the way depending on `toast` itself would
        // (useToast() returns a new object literal every render).
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [t, toast.error]);
    const selectedRelease = SP_REACT.useMemo(() => {
        if (!versionsData)
            return null;
        return versionsData.find((v) => v.tag === selectedTag) || null;
    }, [versionsData, selectedTag]);
    const selectedVersion = selectedRelease?.version ?? "";
    // Format dropdown options
    const versionOptions = SP_REACT.useMemo(() => {
        if (!versionsData)
            return [];
        return versionsData.map((v) => {
            // The rotating "Dev" release's asset filename carries the real
            // build identity (branch + short SHA) — its parsed `version` is
            // always the literal string "Dev".
            const devBuildId = v.prerelease ? extractDevBuildId(v.asset_name) : null;
            let label = devBuildId ?? `v${v.version}`;
            // currentBuildId === null gates the stable branch: dev builds are
            // cut before package.json's version bumps, so a genuinely-installed
            // dev build's `currentVersion` is identical to the officially
            // tagged stable release sharing that frozen base version. Without
            // this guard both rows would show "(installed)" at once whenever a
            // dev build happens to share its base version with a real release.
            const isInstalled = v.prerelease
                ? devBuildId !== null && devBuildId === currentBuildId
                : currentBuildId === null && v.version === currentVersion;
            if (isInstalled) {
                label += ` (${t("updater.installedLabel", {
                    defaultValue: "installed",
                })})`;
            }
            else if (updateData?.latest?.version === v.version) {
                label += ` (${t("updater.latestLabel", { defaultValue: "latest" })})`;
            }
            if (v.prerelease) {
                label += " [DEV]";
            }
            return {
                data: v.tag,
                label,
            };
        });
    }, [versionsData, currentVersion, currentBuildId, updateData, t]);
    const handleVersionSelect = (opt) => {
        const tag = String(opt.data);
        persistentSelectedTag = tag; // survive the QAM dismount that follows selection
        setSelectedTag(tag);
    };
    const handleCheckUpdate = async () => {
        setChecking(true);
        try {
            // Force GitHub to be re-queried (bypasses the 1-hour cache), then
            // pull the now-warm result into the displayed query state — this
            // second pair only hits the in-process cache, no extra GitHub call.
            await Promise.all([forceCheckMut.mutate(), forceVersionsMut.mutate()]);
            await Promise.all([checkUpdate(), refetchVersions()]);
            toast.success(t("updater.checkCompleteTitle", {
                defaultValue: "Update Check Complete",
            }), t("updater.checkCompleteMessage", {
                defaultValue: "Successfully fetched latest version info.",
            }));
        }
        catch (e) {
            const message = e instanceof Error ? e.message : undefined;
            toast.error(t("updater.checkFailedTitle", { defaultValue: "Check Failed" }), message ?? t("errors.unknown"));
        }
        finally {
            setChecking(false);
        }
    };
    const handleShowReleaseNotes = () => {
        if (!selectedRelease)
            return;
        DFL.showModal(SP_JSX.jsx(ReleaseNotesModal, { version: selectedVersion, body: selectedRelease.body }));
    };
    const handleInstall = async () => {
        if (!selectedRelease)
            return;
        const backend = getDeckyBackend();
        if (!backend) {
            toast.error(t("updater.installFailedTitle", { defaultValue: "Install Failed" }), t("updater.noBackend", {
                defaultValue: "Decky backend is unavailable.",
            }));
            return;
        }
        setInstalling(true);
        try {
            // Prerelease/dev tags are mutable — their single GitHub asset gets
            // deleted and re-uploaded under a new name every time a new dev
            // build is cut, so a URL sitting in React state can already be
            // dead. Force a fresh fetch and re-resolve by tag (the stable
            // identifier) before ever handing a URL to Decky's installer.
            let release = selectedRelease;
            if (release.prerelease) {
                const fresh = await forceVersionsMut.mutate();
                // `mutate()` resolves `null` ONLY when the refresh call itself
                // failed (network hiccup, GitHub rate limit, backend error) —
                // a successful call always resolves an array, even an empty
                // one. Treating that failure the same as "genuinely gone" is
                // misleading: it tells the user their release vanished when in
                // fact we simply couldn't check, which is confusing when GitHub
                // shows the release is right there.
                if (fresh === null) {
                    toast.error(t("updater.installFailedTitle", { defaultValue: "Install Failed" }), t("updater.refreshFailedMessage", {
                        defaultValue: "Could not verify the latest release info. Check your connection and try again.",
                    }));
                    setInstalling(false);
                    return;
                }
                const match = fresh.find((v) => v.tag === release.tag);
                if (!match) {
                    toast.error(t("updater.installFailedTitle", { defaultValue: "Install Failed" }), t("updater.releaseGoneMessage", {
                        defaultValue: "This release is no longer available. Please Check for Updates and select again.",
                    }));
                    setInstalling(false);
                    return;
                }
                release = match;
            }
            const { installType, displayVersion } = resolveInstallAction(release, currentVersion, currentBuildId);
            const typeLabel = installType === INSTALL_TYPE_REINSTALL
                ? t("updater.typeReinstall", { defaultValue: "Reinstalling" })
                : installType === INSTALL_TYPE_DOWNGRADE
                    ? t("updater.typeDowngrade", { defaultValue: "Downgrading to" })
                    : t("updater.typeUpdate", { defaultValue: "Updating to" });
            logEvent("triggered", `${typeLabel} v${displayVersion} (type=${installType}) url=${release.asset_url}`);
            toast.info(t("updater.installingTitle", { defaultValue: "Installing Plugin" }), `${typeLabel} v${displayVersion}...`);
            // Hand off to Decky Loader's installer via the GLOBAL ws router.
            // (`call` from @decky/api is plugin-scoped and cannot reach utilities/*.)
            // This only registers the request and pops Decky's native confirm modal
            // (which has its own progress bar); it returns immediately. Decky's modal
            // calls confirm_plugin_install on OK; our listeners mirror the progress.
            await backend.call("utilities/install_plugin", release.asset_url, "Unifideck", displayVersion, release.sha256 || "", installType);
        }
        catch (e) {
            const message = e instanceof Error ? e.message : undefined;
            logEvent("error", message ?? String(e));
            toast.error(t("updater.installFailedTitle", { defaultValue: "Install Failed" }), message ?? t("errors.unknown"));
        }
        finally {
            // The call returns before the install runs; never leave the panel locked.
            // Decky's confirm modal + the loader progress events own the rest.
            setInstalling(false);
        }
    };
    const isLoading = checkingOnMount || loadingVersions;
    const busy = installing || downloadActive || checking;
    // Render header title
    const sectionTitle = SP_REACT.useMemo(() => {
        if (isLoading) {
            return `${t("updater.titleLoading", {
                defaultValue: "Checking version",
            })}...`;
        }
        const buildSuffix = currentBuildId ? ` (${currentBuildId})` : "";
        return `${t("updater.currentTitle", {
            defaultValue: "Current",
        })} - v${currentVersion}${buildSuffix}`;
    }, [currentVersion, currentBuildId, isLoading, t]);
    // Same resolveInstallAction used by handleInstall — previously this
    // button independently re-derived update/downgrade/reinstall via its
    // own compareVersions() call, which could disagree with (and, for
    // prerelease rows, was as wrong as) the logic actually driving the
    // install request itself.
    const installButtonLabel = SP_REACT.useMemo(() => {
        if (downloadActive) {
            return t("updater.installingButton", { defaultValue: "Installing..." });
        }
        if (!selectedRelease)
            return "";
        const { installType, displayVersion } = resolveInstallAction(selectedRelease, currentVersion, currentBuildId);
        if (installType === INSTALL_TYPE_REINSTALL) {
            return t("updater.reinstallButton", {
                version: displayVersion,
                defaultValue: `Reinstall v${displayVersion}`,
            });
        }
        if (installType === INSTALL_TYPE_DOWNGRADE) {
            return t("updater.downgradeButton", {
                version: displayVersion,
                defaultValue: `Downgrade to v${displayVersion}`,
            });
        }
        return t("updater.updateButton", {
            version: displayVersion,
            defaultValue: `Update to v${displayVersion}`,
        });
    }, [downloadActive, selectedRelease, currentVersion, currentBuildId, t]);
    return (SP_JSX.jsx(DFL.PanelSection, { title: sectionTitle, children: isLoading ? (SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("div", { style: { textAlign: "center", padding: "10px", opacity: 0.6 }, children: t("common.loading", { defaultValue: "Loading..." }) }) })) : (SP_JSX.jsxs(SP_JSX.Fragment, { children: [versionOptions.length > 0 && (SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.Dropdown, { rgOptions: versionOptions, selectedOption: selectedTag, onChange: handleVersionSelect, disabled: downloadActive || checking }) })), downloadActive && (SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ProgressBarWithInfo, { layout: "inline", bottomSeparator: "none", nProgress: downloadPercent, sOperationText: downloadStatus }) })), selectedRelease && (SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", onClick: handleInstall, disabled: busy, children: installButtonLabel }) })), selectedRelease && (SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", onClick: handleShowReleaseNotes, disabled: busy, children: t("updater.releaseNotesButton", {
                            defaultValue: "Release Notes",
                        }) }) })), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { layout: "below", onClick: handleCheckUpdate, disabled: busy, children: checking
                            ? t("updater.checkingButton", { defaultValue: "Checking..." })
                            : t("updater.checkButton", {
                                defaultValue: "Check for Updates",
                            }) }) })] })) }));
};

/**
 * useGameActions — install / uninstall / launch / cancel.
 *
 * Wraps `DownloadContext` plus `SteamBridge.runGame` /
 * `terminateApp`, exposing a clean API for any component
 * that needs to trigger a game-state action :
 *  - `install(store, gameId, options?)`
 *  - `uninstall(appId)`
 *  - `cancel(downloadId)`
 *  - `launch(appId, launchOptions)`
 *  - `terminate(appId, force?)`
 *
 * The hook tracks an `isWorking` flag while any one of these
 * is in flight, useful for disabling buttons or showing a
 * spinner without each caller managing its own loading state.
 */

/**
 * Hook bundling all game-level actions a UI element
 * may trigger : install, uninstall, launch, cancel
 * download, force update. Each callback returns a
 * promise so callers can await the backend response
 * before showing a confirmation toast.
 *
 * @param game — the Game the actions apply to.
 * @returns set of memoised action callbacks.
 */
function useGameActions(bridge) {
    const downloads = useDownloads();
    const [isWorking, setWorking] = SP_REACT.useState(false);
    const install = SP_REACT.useCallback(async (store, gameId, options) => {
        setWorking(true);
        try {
            return await downloads.installGame(store, gameId, options);
        }
        finally {
            setWorking(false);
        }
    }, [downloads]);
    const uninstall = SP_REACT.useCallback(async (appId, deletePrefix = false) => {
        setWorking(true);
        try {
            const result = await downloads.uninstallGame(appId, deletePrefix);
            if (result?.success) {
                invalidateGameInfo(appId);
                bumpGameStateVersion(appId);
            }
            return result;
        }
        finally {
            setWorking(false);
        }
    }, [downloads]);
    const cancel = SP_REACT.useCallback(async (downloadId) => {
        setWorking(true);
        try {
            return await downloads.cancelDownload(downloadId);
        }
        finally {
            setWorking(false);
        }
    }, [downloads]);
    const launch = SP_REACT.useCallback((appId) => {
        bridge.runGame(appId);
    }, [bridge]);
    const terminate = SP_REACT.useCallback((appId, force = false) => {
        bridge.terminateApp(String(appId), force);
    }, [bridge]);
    return { isWorking, install, uninstall, cancel, launch, terminate };
}

/**
 * useGameSize — lazy, non-blocking "Space Required" / "Installed
 * Size" fetch.
 *
 * The size lookup lives in its own RPC (`get_game_size_bytes`)
 * rather than in `get_game_info`, because resolving a download
 * size shells out to `legendary info` / `gogdl` (subprocess /
 * network) and can take seconds. `usePlaySection` + the game-info
 * panel both gate on `get_game_info`, so doing the size work there
 * stalled the whole custom UI behind Steam's native section. This
 * hook fetches the size separately, in an effect, exactly like
 * `MetaInline` already does for Last Played — the row renders
 * immediately and the size fills in a moment later.
 *
 * The cache is keyed by `"<appId>:<installed>"`, NOT `appId` alone:
 * the backend returns the *download* size while not-installed and
 * the *on-disk* size once installed, so a game that finishes
 * installing must refetch instead of showing the stale pre-install
 * number. Keying on the install state turns that transition into a
 * natural cache miss (no explicit invalidation needed). The
 * module-level cache also de-dupes the fetch so the play-section
 * `MetaInline` and the info-panel size cell share one round-trip.
 */

const cache$1 = new Map();
const inflight = new Map();
function cacheKey(appId, installed) {
    return `${appId}:${installed ? 1 : 0}`;
}
async function fetchSize(appId, key) {
    const cached = cache$1.get(key);
    if (cached != null)
        return cached;
    const existing = inflight.get(key);
    if (existing)
        return existing;
    const promise = (async () => {
        const raw = await call(rpcRoutes.getGameSizeBytes, appId);
        const bytes = unwrapRpcEnvelope(raw, {
            route: rpcRoutes.getGameSizeBytes,
            throwing: false,
        });
        const value = typeof bytes === "number" && bytes > 0 ? bytes : 0;
        cache$1.set(key, value);
        return value;
    })().finally(() => {
        inflight.delete(key);
    });
    inflight.set(key, promise);
    return promise;
}
/**
 * Resolve the install / download size (bytes) for a Steam shortcut
 * appId. Returns `undefined` until the fetch resolves; `0` means the
 * size is unknown (e.g. Ubisoft / Microsoft, or an offline store).
 *
 * @param appId — Steam shortcut app-id, or null to skip.
 * @param installed — current install state. Changing it refetches
 *   (download size → on-disk size) instead of serving a stale value.
 */
function useGameSize(appId, installed) {
    const key = appId != null ? cacheKey(appId, installed) : null;
    const [size, setSize] = SP_REACT.useState(key != null ? cache$1.get(key) : undefined);
    SP_REACT.useEffect(() => {
        if (appId == null || key == null) {
            setSize(undefined);
            return;
        }
        const cached = cache$1.get(key);
        if (cached != null) {
            setSize(cached);
            return;
        }
        let cancelled = false;
        void fetchSize(appId, key)
            .then((bytes) => {
            if (!cancelled)
                setSize(bytes);
        })
            .catch(() => {
            /* size is best-effort — leave undefined */
        });
        return () => {
            cancelled = true;
        };
    }, [appId, key]);
    return size;
}

/**
 * useGamePlaytime — per-game playtime for the Play section.
 *
 * Fetches our own tracking DB's aggregate for one game via the
 * `get_playtime` RPC (backed by `services/playtime/`), returning the
 * local total, the store's authoritative cross-device total (GOG/Epic,
 * `null` until first synced), and the last-played timestamp.
 *
 * This is the read counterpart to the playtime write path
 * (`notify_game_launched`/`notify_game_stopped` → `GAME_LAUNCHED` →
 * `PlaytimeService`). The Play section previously sourced "Last Played"
 * from Steam's `GetPlaytime` only, which is wrong/empty for non-Steam
 * launcher games — this hook lets the UI prefer our own data.
 *
 * Unlike `useGameSize`, playtime is volatile (it changes after every
 * session ends), so there is no persistent module cache: the hook
 * refetches on each mount. The component is re-mounted whenever the
 * app-details page is opened, so this stays at one round-trip per view.
 */

/**
 * Resolve playtime stats for one game from our tracking DB.
 *
 * @param store — store id (e.g. `"gog"`, `"epic"`), or null/undefined to skip.
 * @param gameId — store-native game id (`Game.id` / `store_game_id`), or
 *   null/undefined to skip.
 * @returns the {@link PlaytimeEntry}, or `undefined` until it resolves /
 *   when skipped. A game we've never recorded resolves to an all-zero entry.
 */
function useGamePlaytime(store, gameId) {
    const [entry, setEntry] = SP_REACT.useState(undefined);
    SP_REACT.useEffect(() => {
        if (!store || !gameId) {
            setEntry(undefined);
            return;
        }
        let cancelled = false;
        void call(rpcRoutes.getPlaytime, store, gameId)
            .then((raw) => {
            if (cancelled)
                return;
            const data = unwrapRpcEnvelope(raw, {
                route: rpcRoutes.getPlaytime,
                throwing: false,
            });
            setEntry(data ?? undefined);
        })
            .catch(() => {
            /* playtime is best-effort — leave undefined (Steam fallback) */
        });
        return () => {
            cancelled = true;
        };
    }, [store, gameId]);
    return entry;
}

/**
 * Play-section focus styling + shared layout utilities.
 *
 * Single CSS payload injected once into <head>. Provides:
 *  - Brand-coloured gradient backgrounds for the five action
 *    buttons (`install`, `play`, `resume`, `stop`, `cancel`,
 *    `update`) on hover / gamepad-focus (`.gpfocus`) / active.
 *  - A `unifideck-slide` keyframe used by the indeterminate
 *    progress bar (extracting / verifying phases).
 *  - Layout helpers (`unifideck-play-row`, `unifideck-meta-strip`,
 *    `unifideck-icon-col`) so each variant component stays
 *    presentation-light.
 *
 * Class-scoped — does not touch any element Unifideck doesn't
 * render itself.
 */

const STYLE_ID = "unifideck-play-focus-styles";
/** Marker class added to the App-Details inner container (in
 *  `AppDetailsPatch`) for non-Steam shortcuts. {@link nativeAppDetailsHideCss}
 *  scopes the native-section hides to subtrees carrying it, so Steam
 *  games (never marked) keep their native UI. */
const HIDE_NATIVE_PLAY_MARKER = "unifideck-hide-native-play";
/**
 * CSS that hides Steam's native App-Details sections we replace for
 * shortcut pages: the native Play row AND the lower tabbed section
 * (Activity / Your Stuff / Community / Game Info — i.e. the whole
 * `AppDetailsContainer`, nav row + tab content).
 *
 * Steam ships *separate* app-details modules for desktop and Gaming
 * Mode (BPM / gamepadui), each with its own obfuscated class for the
 * native Play row:
 *   - Gaming Mode → `basicAppDetailsSectionStylerClasses.PlaySection`
 *     (the module `@decky/ui` matches on `AppDetailsRoot`)
 *   - Desktop     → `playSectionClasses.Container`
 * The tabbed section is `basicAppDetailsSectionStylerClasses
 * .AppDetailsContainer`. All rotate per Steam build, so they're
 * resolved at runtime. These sections render in a *different* subtree
 * from our injected wrapper (so they can't be removed in-tree);
 * instead the patch marks a common ancestor with
 * {@link HIDE_NATIVE_PLAY_MARKER} and these rules hide the targets
 * beneath it. Purely declarative — applies the instant they paint, no
 * RPC / CDP round-trip. We emit a rule for every class we can resolve
 * so the hide lands in whichever UI is rendering. Returns `""` (no-op)
 * if none resolve.
 *
 * Render it inline via `<style>` in our injected subtree (same pattern
 * as {@link PLAY_FOCUS_CSS} / `PlayShell`) so it lands in the right
 * CEF document.
 */
function nativeAppDetailsHideCss() {
    const classes = new Set();
    const add = (v) => {
        if (typeof v === "string" && v)
            classes.add(v);
    };
    // Native Play row (Gaming Mode + desktop variants).
    add(DFL.basicAppDetailsSectionStylerClasses?.PlaySection);
    add(DFL.playSectionClasses?.Container);
    // Lower tabbed section (Activity / Community / Game Info + content).
    add(DFL.basicAppDetailsSectionStylerClasses?.AppDetailsContainer);
    return Array.from(classes)
        .map((c) => `.${HIDE_NATIVE_PLAY_MARKER} [class*="${c}"]{display:none !important;}`)
        .join("\n");
}
/**
 * Minimal position-only nudge for HLTB for Deck's "clean" / "clean-left"
 * BOX modes (`.hltb-info-clean`). HLTB positions that box with `top:-55vh`
 * — a float-hack calibrated for a LOW wrapper. Once `AppDetailsPatch`
 * relocates HLTB's wrapper up to the hero/Play seam (so the `default` /
 * `clean-default` BAR modes render at the hero bottom exactly like native),
 * that same `-55vh` overshoots and throws the box off the top of the
 * screen. We re-anchor it with `bottom` from the seam so it overlays the
 * hero / game logo instead — where the box is meant to sit.
 *
 * Position ONLY: HLTB's transparency (`rgba(14,20,27,0.5)`), box size, and
 * left/right side (`right:2.8vw` / `left:2.8vw`) are all left untouched, so
 * the box still looks exactly as the HLTB devs designed it. Scoped under
 * {@link HIDE_NATIVE_PLAY_MARKER} (Unifideck shortcuts only) and keyed on
 * HLTB's hard-coded class, so it no-ops gracefully if HLTB is absent or
 * renames it. The BAR modes need no CSS — relocation alone places them.
 */
const HLTB_CLEAN_BOX_POSITION_CSS = `
.${HIDE_NATIVE_PLAY_MARKER} #hltb-for-deck .hltb-info-clean{
  top:auto !important;bottom:8px !important;
}
`;
/** Focus / hover styling for every Unifideck-rendered button.
 *  Exported so components can render it inline via `<style>` in
 *  their own subtree — the reliable way to get the rules into the
 *  right CEF document (the App-Details patch and the QuickAccess
 *  panel render in SEPARATE documents, and a `document.head`
 *  injection from one doesn't reach the other). Matches staging's
 *  ``<style>{buttonStyles}</style>`` pattern. */
const PLAY_FOCUS_CSS = `
.unifideck-install-btn,
.unifideck-play-btn,
.unifideck-resume-btn,
.unifideck-stop-btn,
.unifideck-cancel-btn,
.unifideck-update-btn,
.unifideck-icon-btn {
  background: rgba(255, 255, 255, 0.10) !important;
  transition: background 0.15s ease, filter 0.15s ease, transform 0.1s ease !important;
}

.unifideck-install-btn { min-width: 200px; height: 48px; }
.unifideck-play-btn,
.unifideck-resume-btn,
.unifideck-update-btn { min-width: 200px; height: 48px; }
.unifideck-stop-btn,
.unifideck-icon-btn { width: 48px; height: 48px; padding: 0 !important; }
.unifideck-cancel-btn { min-width: 160px; height: 44px; }

/* Focus accent matches Steam: Install / Resume blue, Play green,
 * Cancel red, Update amber. We match three focus signals so the
 * colour lands no matter where Steam attaches the focus marker:
 * the gamepad gpfocus class, native :focus, and :focus-within
 * (when the focusable node is a child of the element carrying our
 * class). */
.unifideck-install-btn:hover,
.unifideck-install-btn:focus,
.unifideck-install-btn:focus-within,
.unifideck-install-btn.gpfocus {
  background: linear-gradient(135deg, #1a9fff 0%, #1570b5 100%) !important;
  box-shadow: 0 0 0 2px rgba(26, 159, 255, 0.55) inset !important;
}
.unifideck-cancel-btn:hover,
.unifideck-cancel-btn:focus,
.unifideck-cancel-btn:focus-within,
.unifideck-cancel-btn.gpfocus {
  background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
  box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.55) inset !important;
}
.unifideck-play-btn:hover,
.unifideck-play-btn:focus,
.unifideck-play-btn:focus-within,
.unifideck-play-btn.gpfocus {
  background: linear-gradient(135deg, #59bf40 0%, #459e31 100%) !important;
  box-shadow: 0 0 0 2px rgba(89, 191, 64, 0.55) inset !important;
}
.unifideck-resume-btn:hover,
.unifideck-resume-btn:focus,
.unifideck-resume-btn:focus-within,
.unifideck-resume-btn.gpfocus {
  background: linear-gradient(135deg, #1a9fff 0%, #1570b5 100%) !important;
  box-shadow: 0 0 0 2px rgba(26, 159, 255, 0.55) inset !important;
}
.unifideck-stop-btn:hover,
.unifideck-stop-btn:focus,
.unifideck-stop-btn:focus-within,
.unifideck-stop-btn.gpfocus,
.unifideck-icon-btn:hover,
.unifideck-icon-btn:focus,
.unifideck-icon-btn:focus-within,
.unifideck-icon-btn.gpfocus {
  background: #ffffff !important;
  color: #1a1a1a !important;
}
.unifideck-stop-btn:hover svg,
.unifideck-stop-btn:focus svg,
.unifideck-stop-btn:focus-within svg,
.unifideck-stop-btn.gpfocus svg,
.unifideck-icon-btn:hover svg,
.unifideck-icon-btn:focus svg,
.unifideck-icon-btn:focus-within svg,
.unifideck-icon-btn.gpfocus svg {
  color: #1a1a1a !important;
  fill: #1a1a1a !important;
}
.unifideck-update-btn:hover,
.unifideck-update-btn:focus,
.unifideck-update-btn:focus-within,
.unifideck-update-btn.gpfocus {
  background: linear-gradient(135deg, #f4b400 0%, #d09100 100%) !important;
  box-shadow: 0 0 0 2px rgba(244, 180, 0, 0.55) inset !important;
}

.unifideck-install-btn:active,
.unifideck-cancel-btn:active,
.unifideck-play-btn:active,
.unifideck-resume-btn:active,
.unifideck-stop-btn:active,
.unifideck-update-btn:active,
.unifideck-icon-btn:active {
  filter: brightness(0.85) !important;
  transform: translateY(1px);
}

/* Two-column row : action(s) on the left, icons on the right. */
.unifideck-play-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 16px;
  background: rgba(14, 20, 27, 0.33);
  border-radius: 6px;
}
.unifideck-action-col {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  flex: 1 1 auto;
}
.unifideck-button-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.unifideck-icon-col {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

/* Metadata side-rail (Space Required, Last Played). */
.unifideck-meta-strip {
  display: flex;
  gap: 16px;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.65);
  letter-spacing: 0.02em;
}
.unifideck-meta-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.unifideck-meta-label {
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: 0.08em;
  color: rgba(255, 255, 255, 0.45);
}
.unifideck-meta-value {
  color: rgba(255, 255, 255, 0.85);
  font-weight: 500;
}

/* Progress bar. */
.unifideck-progress-track {
  height: 4px;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 2px;
  overflow: hidden;
  position: relative;
}
.unifideck-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #1a9fff 0%, #1570b5 100%);
  transition: width 0.3s ease;
  border-radius: 2px;
}
.unifideck-progress-indeterminate {
  position: absolute;
  inset: 0;
  width: 40%;
  background: linear-gradient(90deg, transparent 0%, #1a9fff 50%, transparent 100%);
  animation: unifideck-slide 1.5s linear infinite;
}
/* Decorative indeterminate shimmer: the sweep direction is purely
   cosmetic and need not flip for RTL. */
@keyframes unifideck-slide {
  0%   { transform: translateX(-100%); } /* rtl-ignore */
  100% { transform: translateX(250%); } /* rtl-ignore */
}

.unifideck-status-label {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.85);
  font-weight: 500;
}
.unifideck-status-detail {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.55);
  letter-spacing: 0.02em;
}

/* QuickAccess "Recently finished" → green Play badge. Solid green
 * base; brighten + ring on hover / focus / gamepad-focus so it
 * reads as the active "go" action next to the status badge. */
.unifideck-download-play-btn {
  background: #22c55e !important;
  transition: filter 0.15s ease, box-shadow 0.15s ease !important;
}
.unifideck-download-play-btn:hover,
.unifideck-download-play-btn:focus,
.unifideck-download-play-btn:focus-within,
.unifideck-download-play-btn.gpfocus {
  filter: brightness(1.15) !important;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8) !important;
}

/* Spinning cloud icon while a save sync is in flight. */
@keyframes unifideck-cloud-spin {
  100% { transform: rotate(360deg); }
}
.unifideck-cloud-spin {
  animation: unifideck-cloud-spin 1s linear infinite;
}

/* Breathing animation for save discrepancies. */
@keyframes unifideck-breathe {
  0%, 100% {
    --unifideck-btn-bg: rgba(34, 197, 94, 0.08);
    --unifideck-btn-shadow: 0 0 0 1.5px rgba(34, 197, 94, 0.35) inset, 0 0 0px rgba(34, 197, 94, 0);
  }
  50% {
    --unifideck-btn-bg: rgba(34, 197, 94, 0.45);
    --unifideck-btn-shadow: 0 0 0 2.5px rgba(34, 197, 94, 0.95) inset, 0 0 12px rgba(34, 197, 94, 0.75);
  }
}
.unifideck-breathe:not(:hover):not(:focus):not(:focus-within):not(.gpfocus) {
  background: var(--unifideck-btn-bg, rgba(34, 197, 94, 0.18)) !important;
  box-shadow: var(--unifideck-btn-shadow, 0 0 0 2px rgba(34, 197, 94, 0.5) inset) !important;
  animation: unifideck-breathe 1.5s ease-in-out infinite !important;
}
`;
/** Inject the focus CSS into <head> exactly once.
 *
 *  Prefer rendering {@link PLAY_FOCUS_CSS} inline via `<style>` in
 *  the component subtree (see PlayShell / DownloadsTab) — that's
 *  what reliably lands the rules in the right CEF document. This
 *  head-injection is kept as a harmless fallback. */
function injectPlayFocusStyles() {
    if (typeof document === "undefined")
        return;
    if (document.getElementById(STYLE_ID))
        return;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = PLAY_FOCUS_CSS;
    document.head.appendChild(style);
}

/** Inline style shared by every primary action button
 *  (Install / Play / Resume / Update / Cancel). Matches
 *  staging's ``actionBtnStyle``. */
const actionBtnStyle$1 = {
    display: "flex",
    alignItems: "center",
    justifyContent: "flex-start",
    gap: 8,
    flex: "0 0 auto",
    width: "auto",
    minWidth: 200,
    height: 48,
    padding: "0 24px",
    color: "#fff",
    fontSize: 16,
    fontWeight: 500,
    borderRadius: 4,
    border: "none",
    textTransform: "uppercase",
    letterSpacing: "0.05em",
};
/** Inline style for square icon buttons (controller / settings
 *  / X). Matches staging's icon DialogButton inline style. */
const iconBtnStyle = {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    width: 48,
    height: 48,
    minWidth: 48,
    padding: 0,
    background: "rgba(255, 255, 255, 0.1)",
    borderRadius: 4,
};
/**
 * Outer Focusable shell for every play variant. Styling matches
 * staging: full-width flex row with the translucent dark background
 * and 16 px inner padding.
 *
 * `autoFocus` is a real Steam `Focusable` nav option (consumed into
 * navOptions, NOT the React DOM attribute) — when true, gamepad focus
 * lands on the shell's first focusable child (the primary action
 * button) as the nav node mounts. The variants pass it so the page
 * loads with our Install / Play / Resume / Cancel button focused
 * instead of Steam's (now hidden) native Play button. The loading
 * skeleton omits it.
 */
const PlayShell = ({ children, autoFocus, }) => {
    const ref = SP_REACT.useRef(null);
    // The `autoFocus` navOption only wins if the Focusable exists during
    // Steam's initial app-page focus pass — but our variant mounts *late*
    // (after `get_game_info` resolves, past the loading skeleton), so it
    // often misses. Imperatively focus the primary button on mount, with
    // one delayed retry to beat that pass. Bails once focus has landed so
    // it never steals focus the user moved away.
    SP_REACT.useEffect(() => {
        if (!autoFocus)
            return;
        let raf = 0;
        let timer = 0;
        let retried = false;
        const grab = () => {
            const root = ref.current;
            if (!root)
                return;
            const target = root.querySelector("button") ?? root;
            if (document.activeElement === target)
                return;
            target.focus?.();
            if (document.activeElement !== target && !retried) {
                retried = true;
                timer = window.setTimeout(grab, 140);
            }
        };
        raf = requestAnimationFrame(grab);
        return () => {
            cancelAnimationFrame(raf);
            window.clearTimeout(timer);
        };
    }, [autoFocus]);
    return (SP_JSX.jsxs(DFL.Focusable, { ref: ref, "flow-children": "row", 
        // autoFocus is intentional: claims gamepad focus for the primary action
        // eslint-disable-next-line jsx-a11y/no-autofocus
        autoFocus: autoFocus, style: {
            display: "flex",
            alignItems: "center",
            width: "100%",
            padding: "16px",
            boxSizing: "border-box",
            background: "rgba(14, 20, 27, 0.33)",
            position: "relative",
            zIndex: 2,
        }, children: [SP_JSX.jsx("style", { children: PLAY_FOCUS_CSS }), children] }));
};
/**
 * Placeholder shown in the Play section while `get_game_info` is still
 * resolving for a shortcut. The native Play row is already hidden by
 * CSS at that point, so this keeps the area from collapsing (no layout
 * jump) until the real variant renders. A muted button-sized block —
 * no text, so it needs no i18n.
 */
const PlayLoadingSkeleton = () => (SP_JSX.jsx(PlayShell, { children: SP_JSX.jsx("div", { style: {
            minWidth: 200,
            height: 48,
            borderRadius: 4,
            background: "rgba(255, 255, 255, 0.06)",
        } }) }));
/** Right-floated icon group. ``marginLeft: auto`` pushes it
 *  to the far right of the row. */
const IconGroup = ({ children }) => (SP_JSX.jsx("div", { style: {
        display: "flex",
        gap: 8,
        marginInlineStart: "auto",
        flex: "0 0 auto",
    }, children: children }));
/** Format bytes as a human-readable size string. */
function formatBytes$2(bytes) {
    if (!bytes || bytes <= 0)
        return "—";
    const units = ["B", "KB", "MB", "GB", "TB"];
    let i = 0;
    let v = bytes;
    while (v >= 1024 && i < units.length - 1) {
        v /= 1024;
        i++;
    }
    return `${v.toFixed(v >= 100 ? 0 : 1)} ${units[i]}`;
}
/** Format a Date as a short localized date string. */
function formatDate(d) {
    return d.toLocaleDateString(undefined, {
        year: "numeric",
        month: "short",
        day: "numeric",
    });
}
/** Format a Unix timestamp (seconds) as a date string. */
function formatLastPlayed(rtLastTimePlayed) {
    if (!rtLastTimePlayed || rtLastTimePlayed <= 0)
        return "—";
    return formatDate(new Date(rtLastTimePlayed * 1000));
}
/** Format an ISO-8601 timestamp (our DB's ``last_played_at``) as a
 *  date string, or ``null`` when absent/unparseable so callers can
 *  fall back to Steam's value. */
function formatLastPlayedISO(iso) {
    if (!iso)
        return null;
    const ms = Date.parse(iso);
    if (Number.isNaN(ms))
        return null;
    return formatDate(new Date(ms));
}
/** Format a playtime duration (seconds) as a compact "12.5 h" / "45 min"
 *  string. Returns "—" for zero/unknown. Units are i18n-supplied so the
 *  abbreviation can be localized. */
function formatPlaytime(secs, hoursShort, minutesShort) {
    if (!secs || secs <= 0)
        return "—";
    if (secs < 3600)
        return `${Math.max(1, Math.round(secs / 60))} ${minutesShort}`;
    const hours = secs / 3600;
    return `${hours >= 100 ? Math.round(hours) : hours.toFixed(1)} ${hoursShort}`;
}
/** One label/value column inside {@link MetaInline}. */
const MetaItem = ({ label, value }) => (SP_JSX.jsxs("div", { children: [SP_JSX.jsx("div", { style: {
                fontSize: 11,
                fontWeight: 600,
                textTransform: "uppercase",
                color: "#8f98a0",
                letterSpacing: "0.08em",
                lineHeight: 1,
                marginBottom: 5,
            }, children: label }), SP_JSX.jsx("div", { style: { fontSize: 14, color: "#dcdedf" }, children: value })] }));
/**
 * Inline metadata block between the primary action button
 * and the icon group. Side-by-side "Space Required" /
 * "Last Played" pairs, with ``marginLeft: 20`` to space it
 * off the action button (matches staging spacing).
 */
const MetaInline = ({ sizeBytes, showLastPlayed = false, appId, store, gameId, installed = false, }) => {
    const { t } = useTranslation();
    const [steamLastPlayed, setSteamLastPlayed] = SP_REACT.useState(null);
    // Size is fetched out-of-band (see useGameSize) so a slow store
    // lookup never blocks this row from rendering. Keyed on `installed`
    // so the on-disk size replaces the pre-install download size once
    // the game finishes installing. Prefer the fetched value; fall back
    // to any size the caller already had.
    const fetchedSize = useGameSize(appId ?? null, installed);
    const resolvedSize = fetchedSize && fetchedSize > 0 ? fetchedSize : sizeBytes;
    // Our own tracking DB (set when the caller knows the store/game pair).
    // Preferred source for both the total and last-played; Steam's value
    // is only used as a fallback when we have no local record.
    const playtime = useGamePlaytime(showLastPlayed ? store : null, showLastPlayed ? gameId : null);
    SP_REACT.useEffect(() => {
        if (!showLastPlayed || appId == null)
            return;
        let cancelled = false;
        const apps = window.SteamClient?.Apps;
        if (!apps?.GetPlaytime)
            return;
        apps
            .GetPlaytime(appId)
            .then((res) => {
            if (cancelled)
                return;
            setSteamLastPlayed(res?.rtLastTimePlayed ?? null);
        })
            .catch(() => {
            /* ignore */
        });
        return () => {
            cancelled = true;
        };
    }, [appId, showLastPlayed]);
    // Total played: store's cross-device total (GOG/Epic) when present,
    // else our local total. Only rendered once we have a tracked record.
    const totalSecs = playtime != null
        ? playtime.store_total_secs ?? playtime.total_seconds
        : undefined;
    const showPlayed = showLastPlayed && totalSecs != null && totalSecs > 0;
    // Last played: prefer our DB's timestamp; fall back to Steam's only
    // when we have no local record (e.g. a Steam-native game we don't track).
    const lastPlayedValue = formatLastPlayedISO(playtime?.last_played) ??
        (steamLastPlayed ? formatLastPlayed(steamLastPlayed) : null);
    return (SP_JSX.jsxs("div", { style: {
            display: "flex",
            alignItems: "center",
            gap: 32,
            marginInlineStart: 20,
            flex: "0 1 auto",
        }, children: [SP_JSX.jsx(MetaItem, { label: installed ? t("playMeta.installedSize") : t("playMeta.spaceRequired"), value: formatBytes$2(resolvedSize) }), showPlayed && (SP_JSX.jsx(MetaItem, { label: t("playMeta.played"), value: formatPlaytime(totalSecs, t("playMeta.hoursShort"), t("playMeta.minutesShort")) })), showLastPlayed && (SP_JSX.jsx(MetaItem, { label: t("playMeta.lastPlayed"), value: lastPlayedValue ?? t("playMeta.neverPlayed") }))] }));
};

function formatEta(secs) {
    if (!secs || secs <= 0)
        return "--:--";
    const h = Math.floor(secs / 3600);
    const m = Math.floor((secs % 3600) / 60);
    const s = secs % 60;
    const pad = (n) => n.toString().padStart(2, "0");
    return h > 0 ? `${pad(h)}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
}
function isIndeterminate(phase) {
    return (phase === "extracting" ||
        phase === "verifying" ||
        phase === "manual" ||
        phase === "preparing");
}
function statusLabelKey(status, phase, prev) {
    // Ubisoft (UPC-driven) installs: no real download — show a dedicated
    // label and let the indeterminate path render the phase_message.
    if (phase === "manual")
        return "downloadsTab.installingViaUpcLabel";
    if (phase === "preparing")
        return "downloadsTab.preparingLabel";
    if (phase === "extracting")
        return "downloadsTab.extractingLabel";
    if (phase === "verifying")
        return "downloadsTab.verifyingLabel";
    if (status === "queued") {
        return prev
            ? "downloadsTab.updateQueuedLabel"
            : "downloadsTab.downloadQueuedLabel";
    }
    return prev
        ? "downloadsTab.downloadingUpdateLabel"
        : "downloadsTab.downloadingLabel";
}
const DownloadProgressRow = ({ download, marginInlineStart = 0, }) => {
    const { t } = useTranslation();
    // The indeterminate slide animation lives in play.css.ts. QAM and the
    // App-Details patch render in different CEF documents, so each one
    // needs its own <style> injection — the helper is idempotent.
    SP_REACT.useEffect(() => {
        injectPlayFocusStyles();
    }, []);
    const indeterminate = isIndeterminate(download.download_phase);
    const pct = Math.max(0, Math.min(100, download.progress_percent));
    const prev = Boolean(download.is_update);
    const label = t(statusLabelKey(download.status, download.download_phase, prev));
    // Indeterminate detail line: rendered purely from phase (+ percent) so it
    // is always localized. The backend's ``phase_message`` is hardcoded English
    // and is deliberately NOT displayed — see DownloadsTab i18n.
    const phase = download.download_phase;
    const detail = phase === "preparing"
        ? t("downloadsTab.preparingMessage")
        : phase === "extracting"
            ? t("downloadsTab.extractingMessage")
            : phase === "verifying"
                ? t("downloadsTab.verifyingMessage", { pct: pct.toFixed(1) })
                : phase === "manual"
                    ? t("downloadsTab.installingViaUpcMessage")
                    : t("downloadsTab.finalizingInstallation");
    return (SP_JSX.jsxs("div", { style: {
            display: "flex",
            flexDirection: "column",
            gap: 6,
            marginInlineStart,
            flex: "1 1 auto",
            minWidth: 0,
        }, children: [SP_JSX.jsx("div", { style: {
                    fontSize: 11,
                    fontWeight: 600,
                    textTransform: "uppercase",
                    color: "#8f98a0",
                    letterSpacing: "0.08em",
                }, children: label }), SP_JSX.jsx("div", { style: {
                    height: 4,
                    background: "rgba(255, 255, 255, 0.1)",
                    borderRadius: 2,
                    overflow: "hidden",
                    position: "relative",
                }, children: indeterminate ? (SP_JSX.jsx("div", { className: "unifideck-progress-indeterminate" })) : (SP_JSX.jsx("div", { style: {
                        height: "100%",
                        width: `${pct}%`,
                        background: "#1a9fff",
                        transition: "width 0.3s ease",
                        borderRadius: 2,
                    } })) }), SP_JSX.jsx("div", { style: {
                    display: "flex",
                    justifyContent: "space-between",
                    fontSize: 11,
                    color: "#8f98a0",
                    fontWeight: 500,
                    letterSpacing: "0.02em",
                }, children: indeterminate ? (SP_JSX.jsx("span", { children: detail })) : (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("span", { children: download.total_bytes > 0
                                ? `${formatBytes$2(download.downloaded_bytes)} / ${formatBytes$2(download.total_bytes)}`
                                : `${pct.toFixed(1)}%` }), download.status === "running" && (SP_JSX.jsxs("span", { style: { marginInlineStart: "auto" }, children: [t("downloadsTab.speedMbps", {
                                    speed: download.speed_mbps.toFixed(1),
                                }), " · ", t("downloadsTab.etaLabel", {
                                    eta: formatEta(download.eta_seconds),
                                })] }))] })) })] }));
};

/** Map backend ``DownloadStatus`` to the i18n outcome key
 *  (kept stable across 16 locale files). Backend says
 *  ``"complete"``/``"failed"``/``"running"`` while the i18n
 *  bundle uses ``"completed"``/``"error"``/``"downloading"`` —
 *  the rename would touch every locale, so we adapt instead. */
function outcomeKey(status) {
    if (status === "complete")
        return "completed";
    if (status === "failed")
        return "error";
    if (status === "running")
        return "downloading";
    return status;
}
function outcomeColor(status) {
    // Complete is blue (the Play badge alongside it carries the green
    // "go" accent); cancelled grey; failed red.
    if (status === "complete")
        return "#1a9fff";
    if (status === "cancelled")
        return "#94a3b8";
    return "#ef4444";
}
/** Shared badge geometry so the status badge and the Play badge
 *  read as a matched pair. */
const BADGE_STYLE = {
    fontSize: 11,
    padding: "2px 8px",
    borderRadius: 3,
    fontWeight: 600,
    color: "#0f172a",
    lineHeight: "1.4",
};
const bridge = new SteamBridge();
/**
 * One row of the downloads tab. Renders the artwork,
 * progress bar, status badge, action buttons. Variants
 * (`current` / `queued` / `finished`) tweak the layout
 * and which actions are exposed.
 */
const DownloadItemRow = ({ item, variant }) => {
    const { t } = useTranslation();
    const actions = useGameActions(bridge);
    // Launchable appId for a finished row — only when the download
    // completed AND the game still resolves to an installed shortcut.
    const playAppId = SP_REACT.useMemo(() => {
        if (variant !== "finished" || item.status !== "complete")
            return null;
        const id = resolveAppIdFromStoreGame(item.store, item.game_id);
        // _appType is ignored once the appId is in the cache (which it
        // is — resolveAppIdFromStoreGame read it from the same cache).
        return id != null && getInstalledStatus(id, 0, false) ? id : null;
    }, [variant, item.status, item.store, item.game_id]);
    return (SP_JSX.jsxs("div", { style: {
            display: "flex",
            flexDirection: "column",
            gap: 6,
            padding: 6,
        }, children: [SP_JSX.jsxs("div", { style: {
                    display: "flex",
                    alignItems: "center",
                    gap: 6,
                    minWidth: 0,
                }, children: [SP_JSX.jsx(StoreIcon, { store: item.store, size: 14 }), SP_JSX.jsx("span", { style: {
                            flex: 1,
                            fontWeight: 500,
                            minWidth: 0,
                            overflow: "hidden",
                            textOverflow: "ellipsis",
                        }, children: item.game_title })] }), variant === "finished" &&
                item.status === "failed" &&
                item.error_message && (
            // Surface WHY it failed — previously only the red badge showed,
            // leaving the user with no explanation (e.g. not enough disk space).
            SP_JSX.jsx("span", { style: {
                    fontSize: 11,
                    color: "#94a3b8",
                    lineHeight: "1.4",
                    wordBreak: "break-word",
                }, children: friendlyDownloadError(item.error_message, t) })), variant === "finished" && (SP_JSX.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx("span", { style: { ...BADGE_STYLE, background: outcomeColor(item.status) }, children: t(`downloads.outcome.${outcomeKey(item.status)}`) }), playAppId != null && (
                    // Focusable green "Play" badge (replaces the old play icon
                    // button) — styled to match the status badge beside it.
                    SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-download-play-btn", style: {
                            ...BADGE_STYLE,
                            display: "inline-flex",
                            alignItems: "center",
                            justifyContent: "center",
                            width: "auto",
                            minWidth: 0,
                            height: "auto",
                            flex: "0 0 auto",
                            border: "none",
                            background: "#22c55e",
                        }, onClick: () => actions.launch(playAppId), children: t("downloads.play") }))] })), variant === "current" && (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx(DownloadProgressRow, { download: item }), SP_JSX.jsx(DFL.ButtonItem, { layout: "below", disabled: actions.isWorking, onClick: () => void actions.cancel(item.id), children: t("downloads.cancel") })] })), variant === "queued" && (SP_JSX.jsx(DFL.ButtonItem, { layout: "below", disabled: actions.isWorking, onClick: () => void actions.cancel(item.id), children: t("downloads.removeFromQueue") }))] }));
};

/**
 * Quick Access Menu tab listing every active and
 * recently-finished download. Reactive on
 * DownloadContext so progress bars update in real time
 * without per-row polling.
 */
const DownloadsTab = () => {
    const { t } = useTranslation();
    const { queue, loading } = useDownloads();
    if (loading || !queue)
        return null;
    // Defensive : backend may omit any of these keys on early
    // boot or partial-failure responses. Treat missing as empty.
    const current = queue.current ?? null;
    const queued = queue.queued ?? [];
    const finished = queue.finished ?? [];
    const empty = !current && queued.length === 0 && finished.length === 0;
    if (empty) {
        return (SP_JSX.jsx(DFL.PanelSection, { title: t("downloads.title"), children: SP_JSX.jsx("div", { style: { padding: 16, textAlign: "center", color: "#94a3b8" }, children: t("downloads.empty") }) }));
    }
    return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("style", { children: PLAY_FOCUS_CSS }), current && (SP_JSX.jsx(DFL.PanelSection, { title: t("downloads.current"), children: SP_JSX.jsx(DownloadItemRow, { item: current, variant: "current" }) })), queued.length > 0 && (SP_JSX.jsx(DFL.PanelSection, { title: t("downloads.queued"), children: queued.map((item) => (SP_JSX.jsx(DownloadItemRow, { item: item, variant: "queued" }, item.id))) })), finished.length > 0 && (SP_JSX.jsx(DFL.PanelSection, { title: t("downloads.finished"), children: finished.slice(0, 10).map((item) => (SP_JSX.jsx(DownloadItemRow, { item: item, variant: "finished" }, item.id))) }))] }));
};

/** Last-viewed tab persisted across QAM mount/unmount. */
let persistentActiveTab = "settings";
/**
 * Shared style for the two tab buttons.
 *
 * The QAM panel is narrow and each button is a fixed 50% (`flex: 1`), so the
 * longest labels — French "Téléchargements" (15 chars), and longer still with
 * the live "(NN%)" sync suffix — overran the button's rounded boundary. Tight
 * horizontal padding plus a slightly smaller, *zoom-relative* font (`em`, so it
 * scales with Steam's global UI scale at every resolution) gives the text room
 * to fit; `nowrap` + `overflow: hidden` + `ellipsis` is the safety net so text
 * is clipped *inside* the button (never spills past it) in the extreme case.
 */
const tabButtonStyle = (active) => ({
    flex: 1,
    minWidth: 0,
    padding: "10px 6px",
    fontSize: "0.9em",
    whiteSpace: "nowrap",
    overflow: "hidden",
    textOverflow: "ellipsis",
    fontWeight: active ? 600 : 400,
    opacity: active ? 1 : 0.7,
});
/**
 * Root component of the Decky Loader Quick Access menu.
 * Composes the two tabs (Settings, Downloads) and persists
 * the active tab across QAM open/close.
 */
const QuickAccessPanel = () => {
    const { t } = useTranslation();
    const sync = useSync();
    const [tab, setTabState] = SP_REACT.useState(persistentActiveTab);
    const setTab = (next) => {
        persistentActiveTab = next;
        setTabState(next);
    };
    const downloadsLabel = sync.isSyncing
        ? `${t("tabs.downloads")} (${sync.progress?.progress_percent ?? 0}%)`
        : t("tabs.downloads");
    return (SP_JSX.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [SP_JSX.jsxs("div", { style: { display: "flex", gap: 6, padding: "4px 8px 0" }, children: [SP_JSX.jsx(DFL.DialogButton, { onClick: () => setTab("settings"), style: tabButtonStyle(tab === "settings"), children: t("tabs.settings") }), SP_JSX.jsx(DFL.DialogButton, { onClick: () => setTab("downloads"), style: tabButtonStyle(tab === "downloads"), children: downloadsLabel })] }), SP_JSX.jsxs("div", { style: { paddingBlockStart: 12 }, children: [tab === "settings" && (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx(StoreConnections, {}), SP_JSX.jsx(LibrarySync, {}), SP_JSX.jsx(LanguageSelector, {}), SP_JSX.jsx(GameDetailsViewModeToggle, {}), SP_JSX.jsx(CollectionsToggle, {}), SP_JSX.jsx(PluginUpdater, {}), SP_JSX.jsx(CleanupSection, {}), SP_JSX.jsx(CaptureLogsSection, {})] })), tab === "downloads" && SP_JSX.jsx(DownloadsTab, {})] })] }));
};

/**
 * Steam Store spoofing for non-Steam Unifideck shortcuts.
 *
 * Ported from `staging:src/spoofing/SteamStorePatcher.ts`.
 *
 * Strategy: monkey-patch three Steam getter methods so that when
 * Steam asks for the overview / details of a Unifideck shortcut
 * (synthetic AppID), we return the real Steam Store data for the
 * matched real-Steam-AppID instead. The mappings and rich
 * `appdetails` JSON are pre-fetched by the backend
 * `MetadataService` during sync and read here via two RPCs.
 *
 *   - `get_real_steam_appid_mappings` → `{shortcut_id: real_id}`
 *   - `get_steam_metadata_cache`      → `{real_id: appdetails}`
 *
 * Net effect: Epic / GOG / Amazon / Ubisoft shortcuts in Steam's
 * library show real cover art, store descriptions, Metacritic
 * scores, etc. Launch behaviour is unchanged — clicking Play
 * still routes through the GameAction interceptor to the actual
 * launcher.
 */
let steamAppIdMappings = {};
let storeDataCache = {};
let appDetailsCache = {};
const patchedOverviews = new Set();
/** Resolves once `loadFromBackend()` has completed (mappings +
 *  metadata cache populated). `injectGameToAppinfo` awaits this
 *  so that navigation to a game page before the async init has
 *  finished still triggers artwork injection as soon as the data
 *  is ready. */
let _backendLoadPromise = null;
function getAppStore$1() {
    return window.appStore ?? null;
}
function getAppDetailsStore() {
    return (window
        .appDetailsStore ?? null);
}
function toUnsignedAppId(id) {
    return id < 0 ? id + 0x100000000 : id;
}
function toSignedAppId(id) {
    return id > 0x7fffffff ? id - 0x100000000 : id;
}
function extractIds(data) {
    if (!data)
        return [];
    if (Array.isArray(data)) {
        return data
            .map((x) => x.id)
            .filter((id) => typeof id === "number");
    }
    return Object.keys(data)
        .filter((k) => k.startsWith("category_") || k.startsWith("genre_"))
        .map((k) => Number(k.split("_")[1]))
        .filter((n) => !Number.isNaN(n));
}
function extractLanguages(data) {
    if (!data)
        return [];
    if (typeof data === "string") {
        return data.split(",").map((l) => ({ strLanguageName: l.trim() }));
    }
    return Object.keys(data).map((lang) => ({
        strLanguageName: lang.charAt(0).toUpperCase() + lang.slice(1),
    }));
}
function buildOverview(steamAppId, raw) {
    const rtRelease = raw.release_date?.date
        ? Math.floor(new Date(raw.release_date.date).getTime() / 1000)
        : 0;
    const controllerSupport = raw.controller_support === "full"
        ? 2
        : raw.controller_support === "partial"
            ? 1
            : 0;
    return {
        appid: steamAppId,
        display_name: raw.name ?? "",
        app_type: 1,
        visible_in_game_list: true,
        sort_as: raw.name?.toLowerCase() ?? "",
        rt_original_release_date: rtRelease,
        rt_steam_release_date: rtRelease,
        controller_support: controllerSupport,
        steam_deck_compat_category: 0,
        store_tag: extractIds(raw.genres),
        store_category: extractIds(raw.categories),
        metacritic_score: raw.metacritic?.score ?? 0,
        icon_hash: "",
        header_filename: raw.header_image ?? "",
        library_capsule_filename: raw.capsule_image ?? "",
        BIsShortcut: () => false,
        BIsModOrShortcut: () => false,
        GameID: () => String(steamAppId),
        __from_web_api: true,
    };
}
function buildDetails(steamAppId, raw) {
    const developers = raw.developers ?? [];
    const publishers = raw.publishers ?? [];
    const rtRelease = raw.release_date?.date
        ? Math.floor(new Date(raw.release_date.date).getTime() / 1000)
        : 0;
    const cloud = extractIds(raw.categories).includes(23);
    return {
        unAppID: steamAppId,
        strDisplayName: raw.name ?? "",
        strDeveloperName: developers[0] ?? "",
        strHomepageURL: raw.website ?? "",
        strDescription: raw.short_description ?? "",
        strFullDescription: raw.detailed_description ?? raw.about_the_game ?? "",
        associations: {
            rgDevelopers: developers.map((d) => ({ strName: d, strURL: "" })),
            rgPublishers: publishers.map((p) => ({ strName: p, strURL: "" })),
            rgFranchises: [],
        },
        rtReleaseDate: rtRelease,
        vecPlatforms: [
            raw.platforms?.windows && "windows",
            raw.platforms?.mac && "osx",
            raw.platforms?.linux && "linux",
        ].filter((p) => Boolean(p)),
        vecLanguages: extractLanguages(raw.supported_languages),
        bCloudAvailable: cloud,
        bCloudEnabledForApp: cloud,
        achievements: {
            nAchieved: 0,
            nTotal: raw.achievements?.total ?? 0,
            vecAchievedHidden: [],
            vecHighlight: [],
            vecUnachieved: [],
        },
        eSteamInputControllerMask: raw.controller_support === "full"
            ? 2
            : raw.controller_support === "partial"
                ? 1
                : 0,
        vecDLC: (raw.dlc ?? []).map((id) => ({
            appid: id,
            strName: "",
            bInstalled: false,
        })),
        nScreenshots: raw.screenshots?.length ?? 0,
        lDiskSpaceRequiredBytes: 0,
        vecDeckCompatTestResults: [],
        __from_web_api: true,
    };
}
function injectMetadataIntoOverview(overview) {
    if (!overview)
        return false;
    const rawAppId = typeof overview.appid === "function"
        ? overview.appid()
        : overview.appid;
    if (typeof rawAppId !== "number")
        return false;
    if (patchedOverviews.has(rawAppId))
        return false;
    const signed = toSignedAppId(rawAppId);
    const realId = steamAppIdMappings[signed] ?? steamAppIdMappings[rawAppId];
    if (!realId)
        return false;
    const details = appDetailsCache[realId];
    if (!details)
        return false;
    if (typeof details.strDeveloperName === "string" &&
        details.strDeveloperName) {
        overview.developer = details.strDeveloperName;
        overview.strDeveloperName = details.strDeveloperName;
    }
    const publishers = details.associations?.rgPublishers;
    if (publishers?.[0]?.strName) {
        overview.publisher = publishers[0].strName;
        overview.strPublisherName = publishers[0].strName;
    }
    if (typeof details.strDescription === "string") {
        overview.short_description = details.strDescription;
        overview.strShortDescription = details.strDescription;
    }
    if (typeof details.rtReleaseDate === "number") {
        overview.rt_original_release_date = details.rtReleaseDate;
        overview.rt_steam_release_date = details.rtReleaseDate;
    }
    patchedOverviews.add(rawAppId);
    try {
        overview.TriggerChange?.();
    }
    catch {
        /* ignore */
    }
    return true;
}
async function loadFromBackend() {
    try {
        const mappingsResp = await call(rpcRoutes.getRealSteamAppidMappings);
        if (mappingsResp?.success && mappingsResp.mappings) {
            const out = {};
            for (const [k, v] of Object.entries(mappingsResp.mappings)) {
                const key = Number(k);
                if (!Number.isNaN(key))
                    out[key] = v;
            }
            steamAppIdMappings = out;
        }
    }
    catch (e) {
        console.error("[Unifideck Store Patch] failed to load mappings:", e);
    }
    if (Object.keys(steamAppIdMappings).length === 0)
        return;
    try {
        const metaResp = await call(rpcRoutes.getSteamMetadataCache);
        if (metaResp?.success && metaResp.metadata) {
            for (const [k, raw] of Object.entries(metaResp.metadata)) {
                const steamId = Number(k);
                if (Number.isNaN(steamId))
                    continue;
                storeDataCache[steamId] = buildOverview(steamId, raw);
                appDetailsCache[steamId] = buildDetails(steamId, raw);
            }
        }
    }
    catch (e) {
        console.error("[Unifideck Store Patch] failed to load metadata:", e);
    }
}
/** Force a single shortcut's in-memory overview to be re-spoofed.
 *  Called from the AppDetails route patch when the user opens the
 *  details page of a Unifideck shortcut. */
function forceInjectMetadataForShortcut(shortcutAppId) {
    const appStore = getAppStore$1();
    const unsigned = toUnsignedAppId(shortcutAppId);
    patchedOverviews.delete(unsigned);
    const overview = appStore?.m_mapApps?.get(unsigned);
    if (!overview)
        return false;
    return injectMetadataIntoOverview(overview);
}
/** Fire-and-forget : also persist the spoofed metadata via the
 *  backend `inject_game_to_appinfo` RPC so the spoofing survives a
 *  Steam restart. The current backend handler is a no-op stub. */
async function injectGameToAppinfo(shortcutAppId) {
    // Wait for the async backend load to finish so that
    // steamAppIdMappings is populated. On first navigation this
    // may still be in-flight; on subsequent calls the cached
    // promise resolves immediately.
    if (_backendLoadPromise)
        await _backendLoadPromise;
    if (!steamAppIdMappings[shortcutAppId])
        return;
    forceInjectMetadataForShortcut(shortcutAppId);
    try {
        await call(rpcRoutes.injectGameToAppinfo, shortcutAppId);
    }
    catch (e) {
        console.error("[Unifideck Store Patch] inject_game_to_appinfo failed:", e);
    }
}
/**
 * Apply the Steam-store patches. Returns a handle whose `remove()`
 * restores the original Steam getters. Call from the plugin entry
 * after `SteamBridge` is constructed.
 */
async function applyAppStorePatch() {
    _backendLoadPromise = loadFromBackend();
    await _backendLoadPromise;
    const appStore = getAppStore$1();
    const appDetailsStore = getAppDetailsStore();
    if (!appStore || !appDetailsStore) {
        console.warn("[Unifideck Store Patch] appStore / appDetailsStore unavailable");
        return { remove: () => { } };
    }
    const origGetOverview = appStore.GetAppOverviewByAppID.bind(appStore);
    const origGetDetails = appDetailsStore.GetAppDetails.bind(appDetailsStore);
    const origGetData = appDetailsStore.GetAppData?.bind(appDetailsStore);
    appStore.GetAppOverviewByAppID = (id) => {
        const realId = steamAppIdMappings[id];
        if (!realId)
            return origGetOverview(id);
        void injectGameToAppinfo(id);
        const owned = origGetOverview(realId);
        if (owned)
            return owned;
        return storeDataCache[realId] ?? origGetOverview(id);
    };
    appDetailsStore.GetAppDetails = (id) => {
        const realId = steamAppIdMappings[id];
        if (!realId)
            return origGetDetails(id);
        const owned = origGetDetails(realId);
        if (owned)
            return owned;
        return appDetailsCache[realId] ?? origGetDetails(id);
    };
    if (origGetData) {
        appDetailsStore.GetAppData = (id) => {
            const realId = steamAppIdMappings[id];
            if (!realId)
                return origGetData(id);
            const owned = origGetData(realId);
            if (owned)
                return owned;
            const overview = storeDataCache[realId];
            const details = appDetailsCache[realId];
            if (overview || details)
                return { overview, details };
            return origGetData(id);
        };
    }
    console.log(`[Unifideck Store Patch] active — ${Object.keys(steamAppIdMappings).length} mappings, ${Object.keys(appDetailsCache).length} metadata entries`);
    return {
        remove: () => {
            appStore.GetAppOverviewByAppID = origGetOverview;
            appDetailsStore.GetAppDetails = origGetDetails;
            if (origGetData)
                appDetailsStore.GetAppData = origGetData;
            steamAppIdMappings = {};
            storeDataCache = {};
            appDetailsCache = {};
            patchedOverviews.clear();
        },
    };
}

/**
 * usePlaySection — Steam "Play / Install" button override.
 *
 * Mounted by `<PlaySectionWrapper>`, this hook decides what
 * the Play section should look like for a given Steam appId :
 *
 *  - Steam-native game → no override (returns
 *    `{ shouldOverride: false }`)
 *  - Unifideck game, NOT installed → expose `installAction`
 *  - Unifideck game, IN download queue → expose
 *    `cancelDownloadAction` + the live download item
 *  - Unifideck game, installed → expose `playAction` plus
 *    `uninstallAction`
 *
 * The decision is recomputed reactively when the underlying
 * `useGameInfo` data or the download queue snapshot changes.
 * The render component reads `shouldOverride` and the
 * action functions; it stays presentational.
 */

/**
 * Hook that resolves the current Play-section state
 * for an app : not-installed, downloading, installed,
 * launching, or running. Watches install / launch /
 * download events to recompute on every transition,
 * driving the right Play-button variant.
 *
 * @param appId — Steam shortcut app-id.
 * @returns current `PlaySectionState`.
 */
function usePlaySection(appId) {
    const info = useGameInfo(appId);
    const downloads = useDownloads();
    return SP_REACT.useMemo(() => {
        if (appId == null || !info.data) {
            return { kind: "steam-native", shouldOverride: false };
        }
        const game = info.data;
        // Filter out games whose store is plain Steam — we never override those.
        if (game.store === "steam") {
            return { kind: "steam-native", shouldOverride: false };
        }
        // xCloud (Xbox Cloud Gaming) titles stream in a browser — there's
        // nothing to install, so they're always "playable". Surface a Play
        // state regardless of is_installed; the launcher routes the xcloud
        // games.map sentinel to the Edge kiosk streaming flow.
        if (game.store_tags?.includes("xcloud")) {
            return { kind: "xcloud", shouldOverride: true, appId, gameId: game.id };
        }
        // Check the live queue first — a download in progress
        // takes precedence over `is_installed` flag staleness.
        const inQueue = findInQueue(downloads.queue?.current, game.id) ??
            downloads.queue?.queued.find((d) => d.game_id === game.id) ??
            null;
        if (inQueue) {
            return { kind: "downloading", shouldOverride: true, download: inQueue };
        }
        if (!game.is_installed) {
            return { kind: "not-installed", shouldOverride: true, installable: true };
        }
        return { kind: "installed", shouldOverride: true, appId };
    }, [appId, info.data, downloads.queue]);
}
function findInQueue(current, gameId) {
    if (!current)
        return null;
    return current.game_id === gameId ? current : null;
}

/**
 * useStorageConfig — storage-location actions hook.
 *
 * Wraps the storage-related RPCs (`get_storage_locations`,
 * `set_default_storage_location`, `set_custom_install_path`)
 * behind a single typed surface so settings components stay
 * presentational (no `call()` in the component layer).
 *
 * Cache invariant : the locations list is fetched once on
 * mount and refreshed on every mutation so the default badge
 * is always live.
 */

/** Storage-location query + mutators in one hook. Components
 *  call this instead of issuing their own RPCs so a backend
 *  rename is a one-file change. */
function useStorageConfig() {
    const query = useRPCQuery(rpcRoutes.getStorageLocations, []);
    const setDefaultRPC = useRPC(rpcRoutes.setDefaultStorageLocation);
    const setCustomRPC = useRPC(rpcRoutes.setCustomInstallPath);
    const refresh = SP_REACT.useCallback(async () => {
        await query.refetch();
    }, [query]);
    const setDefault = SP_REACT.useCallback(async (id) => {
        const r = await setDefaultRPC(id);
        if (r?.success)
            await refresh();
        return Boolean(r?.success);
    }, [setDefaultRPC, refresh]);
    const setCustomPath = SP_REACT.useCallback(async (path) => {
        const r = await setCustomRPC(path);
        if (r?.success)
            await refresh();
        return Boolean(r?.success);
    }, [setCustomRPC, refresh]);
    return {
        locations: query.data?.locations ?? [],
        defaultLocation: query.data?.default ?? "internal",
        loading: query.loading,
        refresh,
        setDefault,
        setCustomPath,
    };
}

/**
 * gog-language-match — normalize the wildly inconsistent language
 * labels gogdl emits and match them against the user's locale.
 *
 * GOG games describe their installable languages in no fixed
 * format. Across titles we see the full English name ("English",
 * "Spanish"), native names ("Deutsch", "Français"), 2-letter ISO
 * 639-1 ("en", "es"), 3-letter ISO 639-2 ("eng", "spa") plus GOG
 * legacy quirks ("esp" for Spanish, "br" for Brazilian Portuguese,
 * "cn" for Chinese), BCP-47 tags ("en-US", "pt-BR"), and composite
 * "Name (code)" forms ("English (en)", "Spanish (esp)").
 *
 * `normalizeGogLanguage` collapses any of those to an ISO 639-1
 * base code; `matchGogLanguage` uses it to find which raw option
 * a game offers corresponds to a target locale tag. Both degrade
 * gracefully — an unrecognised label yields `null` rather than a
 * wrong guess, so the install modal simply falls back.
 *
 * Note: matching is at the base-language level (e.g. zh-CN and
 * zh-TW both normalize to "zh"); `matchGogLanguage` tries an exact
 * tag match first so a well-formed "zh-TW" still wins over a bare
 * "Chinese" when both are offered.
 */
/** Each group is `[isoBase, ...aliases]`. Aliases are matched
 *  case-insensitively. The base maps to itself. */
const ALIAS_GROUPS = [
    ["en", "eng", "english"],
    ["fr", "fra", "fre", "french", "français", "francais"],
    ["de", "deu", "ger", "german", "deutsch"],
    ["es", "esp", "spa", "spanish", "español", "espanol", "castellano"],
    ["it", "ita", "italian", "italiano"],
    [
        "pt",
        "por",
        "portuguese",
        "português",
        "portugues",
        "brazilian",
        "br", // GOG legacy code for Brazilian Portuguese
    ],
    ["ru", "rus", "russian", "русский"],
    ["pl", "pol", "polish", "polski"],
    [
        "zh",
        "zho",
        "chi",
        "chinese",
        "cn", // GOG legacy code
        "中文",
        "简体中文",
        "繁體中文",
        "繁体中文",
        "simplified chinese",
        "traditional chinese",
    ],
    ["ja", "jpn", "jp", "japanese", "日本語"],
    ["ko", "kor", "korean", "한국어"],
    ["nl", "nld", "dut", "dutch", "nederlands"],
    ["tr", "tur", "turkish", "türkçe", "turkce"],
    ["uk", "ukr", "ukrainian", "українська"],
    ["cs", "ces", "cze", "czech", "čeština", "cestina"],
    ["hu", "hun", "hungarian", "magyar"],
    ["sv", "swe", "swedish", "svenska"],
    ["da", "dan", "danish", "dansk"],
    ["fi", "fin", "finnish", "suomi"],
    [
        "no",
        "nor",
        "norwegian",
        "norsk",
        "nb",
        "nob",
        "bokmål",
        "bokmal",
        "nn",
        "nno",
        "nynorsk",
    ],
    ["ar", "ara", "arabic", "العربية"],
    ["th", "tha", "thai", "ไทย"],
    ["el", "gre", "ell", "greek", "ελληνικά"],
    ["ro", "ron", "rum", "romanian", "română", "romana"],
    ["bg", "bul", "bulgarian", "български"],
];
const ALIASES = (() => {
    const map = {};
    for (const [base, ...aliases] of ALIAS_GROUPS) {
        map[base] = base;
        for (const alias of aliases)
            map[alias] = base;
    }
    return map;
})();
/** Look up a single cleaned token: try the whole token, then its
 *  base segment before a region/script suffix ("en-us" → "en"). */
function lookupToken(token) {
    const t = token.trim().replace(/_/g, "-");
    if (!t)
        return null;
    if (ALIASES[t])
        return ALIASES[t];
    const base = t.split("-")[0];
    if (base && ALIASES[base])
        return ALIASES[base];
    return null;
}
/** Split a raw label into candidate tokens, unpacking a trailing
 *  "Name (code)" parenthetical into both halves. */
function tokensFrom(raw) {
    const s = raw.trim().toLowerCase();
    const tokens = [];
    const paren = s.match(/^(.*?)\s*\(([^)]*)\)\s*$/);
    if (paren) {
        if (paren[1])
            tokens.push(paren[1].trim());
        if (paren[2])
            tokens.push(paren[2].trim());
    }
    // Whole string with parens flattened, then verbatim.
    tokens.push(s.replace(/[()]/g, " ").trim());
    tokens.push(s);
    return tokens;
}
/**
 * Normalize any GOG language label to an ISO 639-1 base code, or
 * `null` if unrecognised.
 */
function normalizeGogLanguage(raw) {
    if (!raw)
        return null;
    for (const token of tokensFrom(raw)) {
        const hit = lookupToken(token);
        if (hit)
            return hit;
    }
    return null;
}
/**
 * Find which raw option corresponds to `targetTag`. Prefers an
 * exact (case-insensitive, `_`/`-`-normalized) tag match so a
 * well-formed regional tag wins over a generic base match;
 * otherwise falls back to base-language normalization. Returns the
 * matching raw option (verbatim, suitable for passing to gogdl) or
 * `null` when the game offers no equivalent.
 */
function matchGogLanguage(rawOptions, targetTag) {
    if (!targetTag || rawOptions.length === 0)
        return null;
    // 1. Exact tag match.
    const targetLc = targetTag.trim().toLowerCase().replace(/_/g, "-");
    for (const opt of rawOptions) {
        if (opt.trim().toLowerCase().replace(/_/g, "-") === targetLc)
            return opt;
    }
    // 2. Base-language match.
    const target = normalizeGogLanguage(targetTag);
    if (!target)
        return null;
    for (const opt of rawOptions) {
        if (normalizeGogLanguage(opt) === target)
            return opt;
    }
    return null;
}

/** Display labels for GOG language codes. Falls back to the
 *  raw code if a code isn't recognised — the modal still works,
 *  just without the localised name. */
const LANGUAGE_NAMES = {
    "en-US": "English",
    "de-DE": "Deutsch (German)",
    "fr-FR": "Français (French)",
    "es-ES": "Español (Spanish)",
    "it-IT": "Italiano (Italian)",
    "pt-BR": "Português (Brasil)",
    "ru-RU": "Русский (Russian)",
    "pl-PL": "Polski (Polish)",
    "zh-CN": "简体中文 (Simplified Chinese)",
    "zh-Hans": "简体中文 (Simplified Chinese)",
    "zh-TW": "繁體中文 (Traditional Chinese)",
    "ja-JP": "日本語 (Japanese)",
    "ko-KR": "한국어 (Korean)",
    "nl-NL": "Nederlands (Dutch)",
    "tr-TR": "Türkçe (Turkish)",
    "uk-UA": "Українська (Ukrainian)",
    "cs-CZ": "Čeština (Czech)",
    "hu-HU": "Magyar (Hungarian)",
    "sv-SE": "Svenska (Swedish)",
    "da-DK": "Dansk (Danish)",
    "fi-FI": "Suomi (Finnish)",
    "no-NO": "Norsk (Norwegian)",
    "ar-SA": "العربية (Arabic)",
    "th-TH": "ไทย (Thai)",
};
/**
 * Pick the option to pre-select: the user's language if the game
 * offers it, else an English option, else the first listed —
 * normalizing the inconsistent label formats gogdl emits. The
 * user still confirms (or changes) before anything installs.
 */
function pickDefaultLanguage(options, preferredTag) {
    return (matchGogLanguage(options, preferredTag) ??
        matchGogLanguage(options, "en") ??
        options[0]);
}
/**
 * Single-select dropdown of GOG language codes. Confirm =
 * close + invoke `onConfirm(language)` so the parent can
 * call `install_game(..., { language })`.
 */
const GOGLanguageSelectModal = ({ gameTitle, languages, onConfirm, closeModal, preferredTag = instance.language, }) => {
    const { t } = useTranslation();
    const safeLanguages = languages.length > 0 ? languages : ["en-US"];
    const [selected, setSelected] = SP_REACT.useState(() => pickDefaultLanguage(safeLanguages, preferredTag));
    const options = safeLanguages.map((lang) => ({
        data: lang,
        label: LANGUAGE_NAMES[lang] ?? lang,
    }));
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("gogLanguageModal.title"), strDescription: t("gogLanguageModal.description", { title: gameTitle }), strOKButtonText: t("gogLanguageModal.install"), strCancelButtonText: t("common.cancel"), onOK: () => {
            onConfirm(selected);
            closeModal?.();
        }, onCancel: closeModal, bHideCloseIcon: false, children: SP_JSX.jsxs("div", { style: {
                padding: 12,
                background: "rgba(0, 0, 0, 0.2)",
                borderRadius: 8,
            }, children: [SP_JSX.jsx("label", { style: {
                        display: "block",
                        marginBottom: 8,
                        color: "#fff",
                        fontSize: 14,
                    }, children: t("gogLanguageModal.label") }), SP_JSX.jsx(DFL.Dropdown, { rgOptions: options, selectedOption: selected, onChange: (opt) => setSelected(opt.data) })] }) }));
};

/* ---- inline SVG icons ---- */
const FolderIcon = () => (SP_JSX.jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "1em", height: "1em", style: { flexShrink: 0, opacity: 0.7 }, children: SP_JSX.jsx("path", { d: "M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" }) }));
const UpArrowIcon = () => (SP_JSX.jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "1em", height: "1em", children: SP_JSX.jsx("path", { d: "M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z" }) }));
const SORT_OPTIONS = [
    { data: "name", label: "" },
    { data: "name_desc", label: "" },
    { data: "modified_newest", label: "" },
    { data: "modified_oldest", label: "" },
    { data: "created_newest", label: "" },
    { data: "created_oldest", label: "" },
    { data: "size_largest", label: "" },
    { data: "size_smallest", label: "" },
];
const SORT_I18N = {
    name: "storageSettings.sortAZ",
    name_desc: "storageSettings.sortZA",
    modified_newest: "storageSettings.sortModifiedNewest",
    modified_oldest: "storageSettings.sortModifiedOldest",
    created_newest: "storageSettings.sortCreatedNewest",
    created_oldest: "storageSettings.sortCreatedOldest",
    size_largest: "storageSettings.sortSizeLargest",
    size_smallest: "storageSettings.sortSizeSmallest",
};
/**
 * Full-featured directory navigator. Fetches browseable device
 * mount points internally so the picker works standalone inside
 * `showModal` portals.
 */
const StoragePathPicker = ({ startPath, onConfirm }) => {
    const { t } = useTranslation();
    const listDir = useRPC(rpcRoutes.listDirectory);
    const createDir = useRPC(rpcRoutes.createDirectory);
    const getDevices = useRPC(rpcRoutes.getBrowseableDevices);
    const [path, setPath] = SP_REACT.useState(startPath);
    const [dirs, setDirs] = SP_REACT.useState([]);
    const [loading, setLoading] = SP_REACT.useState(true);
    const [showHidden, setShowHidden] = SP_REACT.useState(false);
    const [sortBy, setSortBy] = SP_REACT.useState("name");
    const [creatingFolder, setCreatingFolder] = SP_REACT.useState(false);
    const [newFolderName, setNewFolderName] = SP_REACT.useState("");
    const [unsupported, setUnsupported] = SP_REACT.useState(false);
    /* Browseable devices for the quick-access dropdown. */
    const [devices, setDevices] = SP_REACT.useState([]);
    const [devicesLoading, setDevicesLoading] = SP_REACT.useState(true);
    /* Refs for stable fetchDirectory closure. */
    const showHiddenRef = SP_REACT.useRef(showHidden);
    const sortByRef = SP_REACT.useRef(sortBy);
    showHiddenRef.current = showHidden;
    sortByRef.current = sortBy;
    /* Fetch browseable devices once on mount. */
    SP_REACT.useEffect(() => {
        let cancelled = false;
        (async () => {
            try {
                const r = await getDevices();
                if (!cancelled)
                    setDevices(r.devices);
            }
            catch {
                console.warn("[StoragePathPicker] get_browseable_devices unavailable");
            }
            finally {
                if (!cancelled)
                    setDevicesLoading(false);
            }
        })();
        return () => {
            cancelled = true;
        };
    }, [getDevices]);
    const fetchDirectory = SP_REACT.useCallback(async (target) => {
        setLoading(true);
        try {
            const r = await listDir(target, showHiddenRef.current, sortByRef.current);
            setPath(r.path);
            setDirs(r.directories);
            setUnsupported(false);
        }
        catch {
            console.warn("[StoragePathPicker] list_directory unavailable");
            setUnsupported(true);
        }
        finally {
            setLoading(false);
        }
    }, [listDir]);
    /* Initial load + reload on toggle/sort changes. */
    SP_REACT.useEffect(() => {
        void fetchDirectory(path);
    }, [fetchDirectory, showHidden, sortBy]); // eslint-disable-line react-hooks/exhaustive-deps
    const goUp = () => {
        if (path === "/")
            return;
        const parent = path.substring(0, path.lastIndexOf("/")) || "/";
        void fetchDirectory(parent);
    };
    const navigateTo = (dir) => {
        const next = path === "/" ? `/${dir}` : `${path}/${dir}`;
        void fetchDirectory(next);
    };
    const handleCreateFolder = async () => {
        const name = newFolderName.trim();
        if (!name)
            return;
        const fullPath = `${path}/${name}`;
        try {
            await createDir(fullPath);
            setNewFolderName("");
            setCreatingFolder(false);
            await fetchDirectory(fullPath);
        }
        catch {
            // useRPC surfaces the error — no separate toast needed here.
        }
    };
    /* Device dropdown options — actual mount points from /proc/mounts. */
    const deviceOptions = devices.map((d) => ({
        data: d.path,
        label: d.free_space_gb != null
            ? t("storageSettings.freeSpaceLabel", {
                label: d.label,
                freeSpace: d.free_space_gb,
            })
            : d.label,
    }));
    const currentDevicePath = devices.find((d) => path === d.path || path.startsWith(d.path + "/"))?.path;
    const currentFreeSpace = devices.find((d) => path === d.path || path.startsWith(d.path + "/"))
        ?.free_space_gb ?? 0;
    /* Sort options with translated labels. */
    const sortOptions = SORT_OPTIONS.map((o) => ({
        data: o.data,
        label: t(SORT_I18N[o.data] ?? o.data),
    }));
    return (SP_JSX.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: [SP_JSX.jsxs(DFL.Focusable, { style: { display: "flex", gap: 8, alignItems: "center" }, children: [devicesLoading ? (SP_JSX.jsx("div", { style: { flex: 1, minWidth: 0, padding: "6px 0" }, children: SP_JSX.jsx(DFL.Spinner, {}) })) : deviceOptions.length > 0 ? (SP_JSX.jsx("div", { style: { flex: 1, minWidth: 0 }, children: SP_JSX.jsx(DFL.Dropdown, { rgOptions: deviceOptions, selectedOption: currentDevicePath, onChange: (opt) => {
                                void fetchDirectory(opt.data);
                            }, strDefaultLabel: t("storageSettings.selectDevice") }) })) : null, SP_JSX.jsx(DFL.DialogButton, { onClick: goUp, disabled: path === "/", style: {
                            minWidth: 48,
                            width: 48,
                            height: 40,
                            padding: 0,
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                        }, children: SP_JSX.jsx(UpArrowIcon, {}) })] }), SP_JSX.jsx("div", { style: {
                    fontSize: 12,
                    color: "#b0b0b0",
                    wordBreak: "break-all",
                    padding: "4px 0",
                }, children: path }), SP_JSX.jsxs(DFL.Focusable, { style: { display: "flex", gap: 8, alignItems: "center" }, children: [SP_JSX.jsxs(DFL.Focusable, { onActivate: () => setShowHidden(!showHidden), onClick: () => setShowHidden(!showHidden), style: {
                            display: "flex",
                            alignItems: "center",
                            gap: 8,
                            padding: "6px 12px",
                            cursor: "pointer",
                        }, children: [SP_JSX.jsx(DFL.Toggle, { value: showHidden, onChange: (checked) => setShowHidden(checked) }), SP_JSX.jsx("span", { style: { fontSize: 12, whiteSpace: "nowrap" }, children: t("storageSettings.showHidden") })] }), SP_JSX.jsx("div", { style: { minWidth: 140, flexShrink: 0 }, children: SP_JSX.jsx(DFL.Dropdown, { rgOptions: sortOptions, selectedOption: sortBy, onChange: (opt) => setSortBy(opt.data) }) })] }), SP_JSX.jsxs(DFL.Focusable, { style: {
                    maxHeight: 280,
                    overflowY: "auto",
                    display: "flex",
                    flexDirection: "column",
                    gap: 2,
                }, children: [loading && dirs.length === 0 && (SP_JSX.jsx("div", { style: { color: "#666", fontSize: 12, padding: 8 }, children: t("common.loading") })), !loading && dirs.length === 0 && !unsupported && (SP_JSX.jsx("div", { style: { color: "#666", fontSize: 12, padding: 8 }, children: t("storageSettings.emptyDirectory") })), unsupported && (SP_JSX.jsx(DFL.Field, { label: t("storage.customPath"), description: path, childrenContainerWidth: "fixed" })), dirs.map((d) => (SP_JSX.jsx(DFL.DialogButton, { onClick: () => navigateTo(d), style: {
                            width: "100%",
                            textAlign: "left",
                            fontSize: 13,
                            padding: "8px 12px",
                        }, children: SP_JSX.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx(FolderIcon, {}), SP_JSX.jsx("span", { children: d })] }) }, d)))] }), creatingFolder && (SP_JSX.jsxs(DFL.Focusable, { style: { display: "flex", gap: 6, alignItems: "center" }, children: [SP_JSX.jsx("div", { style: { flex: 1 }, children: SP_JSX.jsx(DFL.TextField, { value: newFolderName, onChange: (e) => setNewFolderName(e.currentTarget.value), focusOnMount: true }) }), SP_JSX.jsx(DFL.DialogButton, { onClick: handleCreateFolder, disabled: !newFolderName.trim(), style: {
                            minWidth: 48,
                            width: 48,
                            height: 40,
                            padding: 0,
                            fontSize: 12,
                        }, children: t("storageSettings.newFolderCreate") }), SP_JSX.jsx(DFL.DialogButton, { onClick: () => {
                            setCreatingFolder(false);
                            setNewFolderName("");
                        }, style: {
                            minWidth: 48,
                            width: 48,
                            height: 40,
                            padding: 0,
                            fontSize: 12,
                        }, children: t("common.cancel") })] })), SP_JSX.jsxs(DFL.Focusable, { style: { display: "flex", gap: 8 }, children: [SP_JSX.jsx(DFL.DialogButton, { onClick: () => onConfirm(path, currentFreeSpace), style: { flex: 1, fontSize: 13 }, children: t("storageSettings.selectFolder") }), !creatingFolder && (SP_JSX.jsx(DFL.DialogButton, { onClick: () => setCreatingFolder(true), style: { flex: 1, fontSize: 13 }, children: t("storageSettings.newFolder") }))] })] }));
};

/* ---- Helpers ---- */
function formatSize$1(bytes) {
    const gb = bytes / 1e9;
    return `${gb.toFixed(1)} GB`;
}
/* ---- Radio row for a storage location ---- */
const LocationRow = ({ selected, onSelect, label, path, freeGb }) => (SP_JSX.jsxs(DFL.Focusable, { onActivate: onSelect, style: {
        display: "flex",
        alignItems: "flex-start",
        gap: 10,
        padding: "10px 12px",
        cursor: "pointer",
    }, children: [SP_JSX.jsx("div", { style: {
                width: 16,
                height: 16,
                borderRadius: "50%",
                border: `2px solid ${selected ? "#dadedf" : "#8f98a0"}`,
                background: selected ? "#dadedf" : "transparent",
                flexShrink: 0,
                marginTop: 2,
            } }), SP_JSX.jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [SP_JSX.jsx("div", { style: { fontSize: 13, fontWeight: 600 }, children: label }), SP_JSX.jsx("div", { style: {
                        fontSize: 11,
                        color: "#8f98a0",
                        wordBreak: "break-all",
                        marginTop: 2,
                    }, children: path }), freeGb > 0 && (SP_JSX.jsxs("div", { style: { fontSize: 11, color: "#8f98a0", marginTop: 1 }, children: [freeGb.toFixed(1), " GB free"] }))] })] }));
/* ---- Component ---- */
const PickStorageModal = ({ gameTitle, gameSizeBytes, locations, defaultLocation, setCustomPath, onConfirm, onCancel, }) => {
    const { t } = useTranslation();
    const available = locations.filter((l) => l.available);
    const [selectedId, setSelectedId] = SP_REACT.useState(defaultLocation);
    const [customPathPicked, setCustomPathPicked] = SP_REACT.useState(null);
    const [customFreeGb, setCustomFreeGb] = SP_REACT.useState(0);
    const [pickExpanded, setPickExpanded] = SP_REACT.useState(false);
    const [saving, setSaving] = SP_REACT.useState(false);
    const handleInstall = SP_REACT.useCallback(async () => {
        setSaving(true);
        try {
            console.log("[PickStorageModal] install: storage=%s customPath=%s", selectedId, customPathPicked ?? "none");
            if (selectedId === "custom" && customPathPicked) {
                console.log("[PickStorageModal] persisting custom path:", customPathPicked);
                await setCustomPath(customPathPicked);
            }
            onConfirm(selectedId, customPathPicked ?? undefined);
        }
        finally {
            setSaving(false);
        }
    }, [selectedId, customPathPicked, setCustomPath, onConfirm]);
    const handleCustomPicked = (path, freeGb) => {
        setCustomPathPicked(path);
        setCustomFreeGb(freeGb);
        setPickExpanded(false);
        setSelectedId("custom");
    };
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("pickStorage.title"), strOKButtonText: saving ? t("common.working") : t("playButton.install"), strCancelButtonText: t("common.cancel"), bOKDisabled: saving || (selectedId === "custom" && !customPathPicked), onOK: handleInstall, onCancel: onCancel, children: SP_JSX.jsxs("div", { style: {
                display: "flex",
                flexDirection: "column",
                gap: 8,
                minWidth: 320,
            }, children: [SP_JSX.jsxs("div", { style: { marginBottom: 4 }, children: [SP_JSX.jsx("div", { style: { fontWeight: 700, fontSize: 14 }, children: gameTitle }), gameSizeBytes != null && gameSizeBytes > 0 && (SP_JSX.jsxs("div", { style: { fontSize: 12, color: "#8f98a0", marginTop: 2 }, children: [t("pickStorage.gameSize"), ": ", formatSize$1(gameSizeBytes)] }))] }), available.map((loc) => {
                    if (loc.id === "custom")
                        return null;
                    return (SP_JSX.jsx(LocationRow, { selected: selectedId === loc.id, onSelect: () => {
                            setSelectedId(loc.id);
                            setPickExpanded(false);
                        }, label: loc.label, path: loc.path, freeGb: loc.free_space_gb }, loc.id));
                }), customPathPicked && !pickExpanded && (SP_JSX.jsx(LocationRow, { selected: selectedId === "custom", onSelect: () => {
                        setSelectedId("custom");
                        setPickExpanded(true);
                        setCustomPathPicked(null);
                    }, label: t("pickStorage.customLabel"), path: customPathPicked, freeGb: customFreeGb })), !customPathPicked && (SP_JSX.jsxs(DFL.Focusable, { onActivate: () => {
                        setSelectedId("custom");
                        setPickExpanded(true);
                    }, style: {
                        display: "flex",
                        alignItems: "flex-start",
                        gap: 10,
                        padding: "10px 12px",
                        cursor: "pointer",
                    }, children: [SP_JSX.jsx("div", { style: {
                                width: 16,
                                height: 16,
                                borderRadius: "50%",
                                border: `2px solid ${selectedId === "custom" ? "#dadedf" : "#8f98a0"}`,
                                background: selectedId === "custom" ? "#dadedf" : "transparent",
                                flexShrink: 0,
                                marginTop: 2,
                            } }), SP_JSX.jsx("div", { style: { fontSize: 13, fontWeight: 600 }, children: t("pickStorage.customOption") })] })), selectedId === "custom" && pickExpanded && (SP_JSX.jsx("div", { style: {
                        padding: "8px 8px 8px 26px",
                        background: "rgba(0,0,0,0.15)",
                        borderRadius: 6,
                    }, children: SP_JSX.jsx(StoragePathPicker, { startPath: locations.find((l) => l.id === "custom")?.path ?? "/home", onConfirm: handleCustomPicked }) }))] }) }));
};
/**
 * Open the storage picker modal and resolve with the user's
 * choice, or `null` if they cancelled.
 *
 * ``onConfirm`` closes the modal and resolves with the pick.
 * ``onCancel`` (Cancel button / B key) resolves with ``null``.
 * A ``settled`` guard prevents double-resolution.
 */
function pickStorageForInstall(gameTitle, gameSizeBytes, locations, defaultLocation, setCustomPath) {
    return new Promise((resolve) => {
        let settled = false;
        const handle = DFL.showModal(SP_JSX.jsx(PickStorageModal, { gameTitle: gameTitle, gameSizeBytes: gameSizeBytes, locations: locations, defaultLocation: defaultLocation, setCustomPath: setCustomPath, onConfirm: (storage, customPath) => {
                if (settled)
                    return;
                settled = true;
                handle?.Close();
                resolve({ storage, customPath });
            }, onCancel: () => {
                if (settled)
                    return;
                settled = true;
                handle?.Close();
                resolve(null);
            } }));
    });
}

/**
 * Wraps install with the per-store prompts required to
 * complete the handshake. Returns `null` if the user
 * cancels a prompt.
 */
function useInstallFlow(bridge) {
    const actions = useGameActions(bridge);
    const getGogLangs = useRPC(rpcRoutes.getGogGameLanguages);
    const getStorageLocations = useRPC(rpcRoutes.getStorageLocations);
    const { locations, defaultLocation, setCustomPath } = useStorageConfig();
    const [working, setWorking] = SP_REACT.useState(false);
    const start = SP_REACT.useCallback(async (game) => {
        setWorking(true);
        try {
            // `useStorageConfig` fetches once at row-mount time, which can be
            // long before the user opens this picker (e.g. an SD card inserted
            // after the library grid rendered). Re-fetch here so newly
            // inserted/removed external drives are always reflected; fall back
            // to the cached values on failure rather than blocking the install.
            let currentLocations = locations;
            let currentDefault = defaultLocation;
            try {
                const fresh = await getStorageLocations();
                currentLocations = fresh.locations;
                currentDefault = fresh.default;
            }
            catch (e) {
                console.log("[useInstallFlow] storage location refresh failed, using cached list", e);
            }
            console.log("[useInstallFlow] opening storage picker for", game.title);
            const picked = await pickStorageForInstall(game.title, game.size_bytes, currentLocations, currentDefault, setCustomPath);
            if (!picked) {
                console.log("[useInstallFlow] storage picker cancelled");
                return null;
            }
            const { storage, customPath } = picked;
            console.log("[useInstallFlow] picked storage=%s customPath=%s", storage, customPath ?? "none");
            if (game.store !== "gog") {
                console.log("[useInstallFlow] installing %s/%s with storage=%s", game.store, game.store_game_id, storage);
                return await actions.install(game.store, game.store_game_id, {
                    storage,
                    title: game.title,
                });
            }
            const langs = await getGogLangs(game.store_game_id).catch(() => null);
            const list = langs?.languages ?? [];
            if (list.length <= 1) {
                const language = list[0];
                return await actions.install(game.store, game.store_game_id, {
                    language,
                    storage,
                    title: game.title,
                });
            }
            const language = await pickLanguageViaModal(game.title, list);
            if (!language)
                return null;
            return await actions.install(game.store, game.store_game_id, {
                language,
                storage,
                title: game.title,
            });
        }
        finally {
            setWorking(false);
        }
    }, [
        actions,
        getGogLangs,
        getStorageLocations,
        locations,
        defaultLocation,
        setCustomPath,
    ]);
    return { isWorking: working || actions.isWorking, start };
}
/** Promise-wrapped showModal that resolves with the picked
 *  language (or null on cancel). Pre-selects the active UI
 *  language (which reflects the "auto" preference resolved to the
 *  system language) when the game offers it. */
function pickLanguageViaModal(title, languages) {
    return new Promise((resolve) => {
        let confirmed = false;
        const handle = DFL.showModal(SP_JSX.jsx(GOGLanguageSelectModal, { gameTitle: title, languages: languages, preferredTag: instance.language, onConfirm: (lang) => {
                confirmed = true;
                resolve(lang);
            }, closeModal: () => {
                handle?.Close();
                if (!confirmed)
                    resolve(null);
            } }));
    });
}

/**
 * Reuse Steam's native app context menu (Add to Favorites / Add to /
 * Remove from / Manage / Developer / Properties) — the popup the native
 * "Manage" gear opens — for our own custom settings button.
 *
 * We hide the native Play section with CSS, but it stays mounted in the
 * DOM with its handlers fully wired. Rather than reconstruct the menu
 * from minified internals (the menu component / showContextMenu export
 * names rotate per Steam build), we locate the native "Manage" button
 * inside that hidden section and invoke its React `onClick` verbatim,
 * passing OUR button as the menu anchor (`currentTarget`). The native
 * handler only reads `currentTarget` for positioning; the menu's items
 * act on the app bound in the handler's closure (the current page's
 * app), so it works for non-Steam shortcuts too.
 *
 * Verified live: the native handler is `i => { if (BKioskModeLocked())…
 * showContextMenu(<AppContextMenu {...}/>, i.currentTarget, opts) }`.
 */
/**
 * Identify the native "Manage" (menu) button among a Play section's
 * buttons by stable, readable markers in its (minified) onClick — the
 * kiosk-lock guard and the menu overlap option. Avoids relying on the
 * localized "Manage" aria-label or on child ordering.
 */
function isManageHandler(fn) {
    if (typeof fn !== "function")
        return false;
    const src = fn.toString();
    return src.includes("BKioskModeLocked") || src.includes("bOverlapHorizontal");
}
/** Read a DOM node's React `onClick` if it's the native menu handler. */
function nativeMenuOnClick(el) {
    const key = Object.keys(el).find((k) => k.startsWith("__reactProps$"));
    if (!key)
        return undefined;
    const props = el[key];
    return isManageHandler(props?.onClick)
        ? props.onClick
        : undefined;
}
/**
 * Open Steam's native app "Manage" context menu anchored to `sourceEl`.
 * Returns `false` if the native button can't be located so the caller
 * can fall back (e.g. to opening Properties directly).
 */
function openNativeAppManageMenu(sourceEl) {
    if (!sourceEl)
        return false;
    // CRITICAL: query `sourceEl.ownerDocument`, NOT the global `document`.
    // The plugin runs in SharedJSContext, whose `document` is a *different*
    // CEF document than the Gaming Mode window where the UI actually renders
    // — so global `document.querySelectorAll` finds nothing. `sourceEl` (our
    // clicked gear) lives in the right document, and the hidden-but-mounted
    // native Play section is in that same document.
    //
    // We scan document-wide (not scoping to the Play-section class) and match
    // the native "Manage" button by its handler signature — that uniquely
    // identifies the one Manage button on the page (verified: 1 match).
    const doc = sourceEl.ownerDocument ?? document;
    for (const btn of Array.from(doc.querySelectorAll("button, [role='button'], [tabindex]"))) {
        const onClick = nativeMenuOnClick(btn);
        if (onClick) {
            onClick({ currentTarget: sourceEl });
            return true;
        }
    }
    return false;
}

const defaultBridge$3 = new SteamBridge();
function openControllerConfig$2(appId) {
    window.SteamClient?.Apps?.ShowControllerConfigurator?.(appId);
}
function openAppSettings$2(appId) {
    window.SteamClient?.Apps?.OpenAppSettingsDialog?.(appId, "general");
}
const NotInstalledButtons = ({ appId, bridge = defaultBridge$3, }) => {
    const { t } = useTranslation();
    const { data: game, loading } = useGameInfo(appId);
    const installFlow = useInstallFlow(bridge);
    const toast = useToast();
    const onInstall = SP_REACT.useCallback(async () => {
        if (!game)
            return;
        const result = await installFlow.start(game);
        if (result == null)
            return;
        if (result.success) {
            toast.success(t("toasts.downloadQueued"), t("toasts.downloadQueuedBody", { title: game.title }));
        }
        else {
            toast.error(t("toasts.downloadFailed"), result.error ?? t("toasts.downloadFailedUnknown"));
        }
    }, [installFlow, game, t, toast]);
    return (
    // autoFocus is intentional: claims gamepad focus for the primary action
    // eslint-disable-next-line jsx-a11y/no-autofocus
    SP_JSX.jsxs(PlayShell, { autoFocus: true, children: [SP_JSX.jsxs(DFL.DialogButton, { className: "unifideck-install-btn", disabled: loading || installFlow.isWorking || !game, onClick: onInstall, style: actionBtnStyle$1, children: [SP_JSX.jsx(FaDownload, {}), installFlow.isWorking
                        ? t("playButton.installing")
                        : t("playButton.install")] }), SP_JSX.jsx(MetaInline, { sizeBytes: game?.size_bytes, showLastPlayed: true, appId: appId, store: game?.store, gameId: game?.id }), SP_JSX.jsxs(IconGroup, { children: [SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-icon-btn", style: iconBtnStyle, onClick: () => openControllerConfig$2(appId), "aria-label": t("playButton.controllerConfig"), children: SP_JSX.jsx(FaGamepad, {}) }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-icon-btn", style: iconBtnStyle, onClick: (e) => {
                            // Open Steam's native app menu (Manage / Properties / …),
                            // matching the native gear; fall back to Properties directly.
                            if (!openNativeAppManageMenu(e?.currentTarget)) {
                                openAppSettings$2(appId);
                            }
                        }, "aria-label": t("playButton.appSettings"), children: SP_JSX.jsx(FaCog, {}) })] })] }));
};

const defaultBridge$2 = new SteamBridge();
const DownloadingButtons = ({ download, bridge = defaultBridge$2, }) => {
    const { t } = useTranslation();
    const actions = useGameActions(bridge);
    const toast = useToast();
    const [cancelled, setCancelled] = SP_REACT.useState(false);
    const doCancel = SP_REACT.useCallback(async () => {
        setCancelled(true);
        const result = await actions.cancel(download.id);
        if (!result?.success) {
            setCancelled(false);
            toast.error(t("toasts.cancelFailed"), result?.error ?? "");
        }
    }, [actions, download.id, t, toast]);
    const onCancelClick = SP_REACT.useCallback(() => {
        if (cancelled)
            return;
        DFL.showModal(SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("play.cancelConfirmTitle"), strDescription: t("play.cancelConfirmBody", {
                title: download.game_title,
            }), strOKButtonText: t("play.cancelConfirmConfirm"), strCancelButtonText: t("play.cancelConfirmCancel"), bDestructiveWarning: true, onOK: () => {
                void doCancel();
            } }));
    }, [cancelled, doCancel, download.game_title, t]);
    return (
    // autoFocus is intentional: claims gamepad focus for the primary action
    // eslint-disable-next-line jsx-a11y/no-autofocus
    SP_JSX.jsxs(PlayShell, { autoFocus: true, children: [SP_JSX.jsxs(DFL.DialogButton, { className: "unifideck-cancel-btn", disabled: cancelled || actions.isWorking, onClick: onCancelClick, style: actionBtnStyle$1, children: [SP_JSX.jsx(FaTimes, {}), cancelled
                        ? t("play.cancelling")
                        : download.download_phase === "manual" ||
                            download.download_phase === "preparing"
                            ? t("play.cancel")
                            : `${t("play.cancel")} (${Math.round(Math.max(0, Math.min(100, download.progress_percent)))}%)`] }), SP_JSX.jsx(DownloadProgressRow, { download: download, marginInlineStart: 20 })] }));
};

const UninstallConfirmModal = ({ gameId: _gameId, gameTitle, onConfirm, closeModal, }) => {
    const { t } = useTranslation();
    const [deletePrefix, setDeletePrefix] = SP_REACT.useState(false);
    return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("style", { children: `
        .unifideck-uninstall-modal + div { display: none !important; }
        .DialogFooter { display: none !important; }
      ` }), SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("uninstallModal.title"), strDescription: "", bHideCloseIcon: false, onOK: closeModal, onCancel: closeModal, children: SP_JSX.jsxs("div", { className: "unifideck-uninstall-modal", style: { padding: "10px 0" }, children: [SP_JSX.jsx("div", { style: {
                                marginBottom: 16,
                                color: "#ccc",
                                fontSize: 14,
                                lineHeight: 1.5,
                            }, children: t("uninstallModal.description", { title: gameTitle }) }), SP_JSX.jsx("div", { style: {
                                marginBottom: 12,
                                padding: 12,
                                backgroundColor: "rgba(0, 0, 0, 0.2)",
                                borderRadius: 8,
                            }, children: SP_JSX.jsx(DFL.ToggleField, { label: t("uninstallModal.deleteProtonLabel"), description: t("uninstallModal.deleteProtonDescription"), checked: deletePrefix, onChange: setDeletePrefix }) }), deletePrefix && (SP_JSX.jsxs("div", { style: {
                                display: "flex",
                                alignItems: "flex-start",
                                gap: 10,
                                padding: 12,
                                backgroundColor: "rgba(239, 68, 68, 0.15)",
                                borderRadius: 8,
                                marginBottom: 16,
                                border: "1px solid rgba(239, 68, 68, 0.3)",
                            }, children: [SP_JSX.jsx(FaExclamationTriangle, { style: { color: "#ef4444", marginTop: 2, flexShrink: 0 } }), SP_JSX.jsxs("div", { style: { color: "#fca5a5", fontSize: 13, lineHeight: 1.4 }, children: [SP_JSX.jsx("strong", { children: t("uninstallModal.warningTitle") }), " ", t("uninstallModal.warningBody")] })] })), SP_JSX.jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [SP_JSX.jsx(DFL.DialogButton, { onClick: closeModal, style: { minWidth: 100 }, children: t("uninstallModal.cancel") }), SP_JSX.jsxs(DFL.DialogButton, { onClick: async () => {
                                        closeModal();
                                        await onConfirm(deletePrefix);
                                    }, style: {
                                        minWidth: 100,
                                        color: "#ef4444",
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: 6,
                                    }, children: [SP_JSX.jsx(FaTrash, {}), " ", t("uninstallModal.uninstall")] })] })] }) })] }));
};

/**
 * useCloudSaveStatus — out-of-band cloud-save status for the
 * cloud-save button.
 *
 * Deliberately NOT folded into `useGameInfo` (the App-Details hot
 * path that must stay non-blocking). `get_cloud_save_status` can hit
 * the store's metadata/CLI, so it is fetched separately — the same
 * way `check_game_update` and `useGameSize` are kept off the render
 * path. Only enabled for GOG/Epic (the stores with cloud-save sync).
 *
 * Live-updates: subscribes to the four CLOUD_SYNC_* events and
 * refetches when one fires for this game, so the button repaints when
 * an auto-pull (or another device's push) completes.
 */

const CLOUD_SAVE_STORES = new Set(["gog", "epic"]);
function useCloudSaveStatus(store, gameId) {
    const enabled = !!store && !!gameId && CLOUD_SAVE_STORES.has(store);
    const query = useRPCQuery(rpcRoutes.getCloudSaveStatus, [store ?? "", gameId ?? ""], { enabled });
    const onSyncEvent = SP_REACT.useCallback((payload) => {
        if (payload.store === store && payload.game_id === gameId) {
            void query.refetch();
        }
    }, [store, gameId, query]);
    useEventBus(Events.CLOUD_SYNC_DOWN_COMPLETE, onSyncEvent, [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_DOWN_FAILED, onSyncEvent, [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_UP_COMPLETE, onSyncEvent, [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_UP_FAILED, onSyncEvent, [store, gameId]);
    useEventBus(Events.GAME_STOPPED, onSyncEvent, [store, gameId]);
    return { ...query, enabled };
}

const pending = new Set();
const key = (store, gameId, dir) => `${dir}:${store}:${gameId}`;
function markCloudOpPending(store, gameId, dir) {
    pending.add(key(store, gameId, dir));
}
/** Returns true (and clears) iff a manual op was pending for this game/dir. */
function consumeCloudOpPending(store, gameId, dir) {
    const k = key(store, gameId, dir);
    const had = pending.has(k);
    pending.delete(k);
    return had;
}

function formatTs$1(ts) {
    if (!ts)
        return "—";
    return new Date(ts * 1000).toLocaleString(undefined, {
        month: "short",
        day: "numeric",
        year: "numeric",
        hour: "numeric",
        minute: "2-digit",
    });
}
function formatBytes$1(n) {
    if (!n)
        return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    let value = n;
    let i = 0;
    while (value >= 1024 && i < units.length - 1) {
        value /= 1024;
        i += 1;
    }
    return `${value.toFixed(i > 0 && value < 10 ? 1 : 0)} ${units[i]}`;
}
const rowStyle$1 = {
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
};
const actionBtnStyle = {
    minWidth: 180,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    gap: 8,
};
const CloudSaveModal = ({ store, gameId, gameTitle, initialStatus, closeModal, }) => {
    const { t } = useTranslation();
    const toast = useToast();
    // The modal owns a live status query so the Local/Cloud rows refresh in
    // place when the background sync completes. Seed from the status the button
    // already fetched to avoid a loading flash on open.
    const live = useCloudSaveStatus(store, gameId);
    const status = live.data ?? initialStatus;
    const pull = useRPCMutation(rpcRoutes.cloudSavePull);
    const push = useRPCMutation(rpcRoutes.cloudSavePush);
    const setPath = useRPCMutation(rpcRoutes.setGameSavePath);
    const [picking, setPicking] = SP_REACT.useState(false);
    // Local "just clicked" flag bridges the gap until the backend lock shows up
    // in a refetch; cleared when a CLOUD_SYNC_* event for this game arrives.
    const [triggered, setTriggered] = SP_REACT.useState(false);
    const clearTriggered = (p) => {
        if (p.store === store && p.game_id === gameId)
            setTriggered(false);
    };
    useEventBus(Events.CLOUD_SYNC_DOWN_COMPLETE, clearTriggered, [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_DOWN_FAILED, clearTriggered, [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_UP_COMPLETE, clearTriggered, [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_UP_FAILED, clearTriggered, [store, gameId]);
    const syncing = triggered || !!status.in_progress;
    const busy = syncing || pull.loading || push.loading;
    const local = status.local_snapshot ?? {};
    const remote = status.remote_snapshot;
    // Fire-and-forget: the backend runs the sync in the background and returns
    // immediately, so the RPC never times out on a long download. We DON'T close
    // — the live hook refetches on the CLOUD_SYNC_* event and the rows update in
    // place. The CloudSaveButton shows the result toast (markCloudOpPending
    // marks this as a user-initiated op so the auto-pull never toasts).
    const onDownload = async () => {
        markCloudOpPending(store, gameId, "down");
        setTriggered(true);
        toast.info(t("toasts.cloudPullStarted"));
        await pull.mutate(store, gameId, true);
        EventBusClient.bumpToFast();
        void live.refetch();
    };
    const onUpload = async () => {
        markCloudOpPending(store, gameId, "up");
        setTriggered(true);
        toast.info(t("toasts.cloudPushStarted"));
        await push.mutate(store, gameId);
        EventBusClient.bumpToFast();
        void live.refetch();
    };
    const onPickFolder = async (path) => {
        setPicking(false);
        const r = await setPath.mutate(store, gameId, path);
        if (r !== null) {
            toast.success(t("toasts.cloudSavePathSet"));
            void live.refetch();
        }
        else {
            toast.error(t("toasts.cloudSavePathFailed"));
        }
    };
    // Both directions need a REAL resolved save location — without one (no
    // prefix yet) there's nowhere the game reads from, so we don't sync into a
    // staging dir. The unresolved banner guides the user to launch / set a path.
    const canDownload = !busy && status.save_path_resolved && status.cloud_supported !== false;
    const canUpload = !busy && status.save_path_resolved && status.has_local_saves;
    // Build a snapshot line from ONLY the fields we actually know (0 = unknown).
    // The cloud size often isn't available from the store's listing — showing
    // nothing is better than an inaccurate (mirror-derived) value.
    const describeSnap = (snap) => {
        const parts = [];
        if (snap.file_count)
            parts.push(t("play.cloudSave.filesCount", { count: snap.file_count }));
        if (snap.total_bytes)
            parts.push(formatBytes$1(snap.total_bytes));
        if (snap.timestamp)
            parts.push(formatTs$1(snap.timestamp));
        return parts.length ? parts.join(" · ") : t("play.cloudSave.noCloudData");
    };
    // Inline folder picker mode — replaces the snapshot panel.
    if (picking) {
        return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("play.cloudSave.setLocationTitle"), strDescription: t("play.cloudSave.setLocationHint"), bAlertDialog: true, strOKButtonText: t("common.cancel"), onOK: () => setPicking(false), onCancel: () => setPicking(false), children: SP_JSX.jsx(StoragePathPicker, { startPath: status.browse_start || "/home", onConfirm: (path) => void onPickFolder(path) }) }));
    }
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("play.cloudSave.menuTitle", { game: gameTitle }), strDescription: "", bAlertDialog: true, strOKButtonText: t("play.cloudSave.close"), onOK: closeModal, children: SP_JSX.jsxs("div", { style: { padding: "10px 0" }, children: [!status.save_path_resolved && (SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        flexDirection: "column",
                        gap: 8,
                        marginBottom: 14,
                        padding: 10,
                        borderRadius: 8,
                        background: "rgba(244, 180, 0, 0.08)",
                    }, children: [SP_JSX.jsxs("div", { style: {
                                display: "flex",
                                alignItems: "center",
                                gap: 10,
                                color: "#ffc107",
                                fontSize: 13,
                            }, children: [SP_JSX.jsx(FaExclamationTriangle, { size: 18 }), SP_JSX.jsx("span", { children: t("play.cloudSave.statusUnresolved") })] }), SP_JSX.jsx("span", { style: { fontSize: 12, color: "#bbb" }, children: t("play.cloudSave.unresolvedHint") }), SP_JSX.jsxs(DFL.DialogButton, { onClick: () => setPicking(true), style: {
                                ...actionBtnStyle,
                                minWidth: 0,
                                alignSelf: "flex-start",
                            }, children: [SP_JSX.jsx(FaFolderOpen, {}), " ", t("play.cloudSave.setLocation")] })] })), SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        flexDirection: "column",
                        gap: 10,
                        marginBottom: 18,
                        backgroundColor: "rgba(255,255,255,0.05)",
                        padding: 15,
                        borderRadius: 8,
                    }, children: [SP_JSX.jsxs("div", { style: rowStyle$1, children: [SP_JSX.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx(FaDesktop, { size: 16 }), " ", SP_JSX.jsx("span", { children: t("cloudSave.local") })] }), SP_JSX.jsx("span", { style: { fontSize: 12, color: "#aaa" }, children: status.has_local_saves
                                        ? describeSnap(local)
                                        : t("play.cloudSave.statusNoSaves") })] }), SP_JSX.jsxs("div", { style: rowStyle$1, children: [SP_JSX.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx(FaCloud, { size: 16 }), " ", SP_JSX.jsx("span", { children: t("cloudSave.cloud") })] }), SP_JSX.jsx("span", { style: { fontSize: 12, color: "#aaa" }, children: remote
                                        ? describeSnap(remote)
                                        : status.cloud_supported === false
                                            ? t("play.cloudSave.noCloudSupport")
                                            : t("play.cloudSave.noCloudData") })] })] }), SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        gap: 10,
                        justifyContent: "center",
                        marginBottom: 6,
                    }, children: [SP_JSX.jsxs(DFL.DialogButton, { disabled: !canDownload, onClick: onDownload, style: actionBtnStyle, children: [SP_JSX.jsx(FaCloudDownloadAlt, {}), " ", t("play.cloudSave.download")] }), SP_JSX.jsxs(DFL.DialogButton, { disabled: !canUpload, onClick: onUpload, style: actionBtnStyle, children: [SP_JSX.jsx(FaCloudUploadAlt, {}), " ", t("play.cloudSave.upload")] })] }), syncing && (SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "center",
                        gap: 8,
                        fontSize: 12,
                        color: "#1a9fff",
                        marginTop: 8,
                    }, children: [SP_JSX.jsx(FaSyncAlt, { className: "unifideck-cloud-spin" }), t("play.cloudSave.statusSyncing")] })), !syncing && status.auto_pull && (SP_JSX.jsx("div", { style: {
                        fontSize: 11,
                        color: "rgba(255,255,255,0.45)",
                        textAlign: "center",
                        marginTop: 4,
                    }, children: t("play.cloudSave.autoPullOn") }))] }) }));
};

/** Local/cloud save-timestamp delta (seconds) treated as "in sync". Absorbs
 *  gogdl truncating the stored cloud mtime to whole seconds vs the local
 *  file's sub-second st_mtime; a real new save is always far larger. */
const TS_SYNC_TOLERANCE_S = 2;
const CloudSaveButton = ({ store, gameId, gameTitle }) => {
    const { t } = useTranslation();
    const toast = useToast();
    const { data, enabled, loading } = useCloudSaveStatus(store, gameId);
    // Manual Download/Upload are fire-and-forget; their result arrives as a
    // CLOUD_SYNC_* event. Toast the outcome here (the triggering modal has
    // closed) — but ONLY for user-initiated ops (consumeCloudOpPending), so the
    // automatic on-launch pull never spams a toast. (useCloudSaveStatus already
    // refetches the status on these same events.)
    const onSyncEvent = (dir, ok, okKey, failKey) => (p) => {
        if (p.store !== store || p.game_id !== gameId)
            return;
        if (!consumeCloudOpPending(store, gameId, dir))
            return;
        if (ok)
            toast.success(t(okKey));
        else
            toast.error(t(failKey), String(p.error ?? ""));
    };
    useEventBus(Events.CLOUD_SYNC_DOWN_COMPLETE, onSyncEvent("down", true, "toasts.cloudPullDone", "toasts.cloudPullFailed"), [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_DOWN_FAILED, onSyncEvent("down", false, "toasts.cloudPullDone", "toasts.cloudPullFailed"), [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_UP_COMPLETE, onSyncEvent("up", true, "toasts.cloudPushDone", "toasts.cloudPushFailed"), [store, gameId]);
    useEventBus(Events.CLOUD_SYNC_UP_FAILED, onSyncEvent("up", false, "toasts.cloudPushDone", "toasts.cloudPushFailed"), [store, gameId]);
    // Not a cloud-save store, or the backend says this store isn't supported.
    if (!enabled)
        return null;
    if (data && !data.supported)
        return null;
    const syncing = !!data?.in_progress;
    const noCloudSupport = data?.cloud_supported === false;
    const unresolved = !!data && !data.save_path_resolved;
    const local = data?.local_snapshot ?? {};
    const remote = data?.remote_snapshot;
    const cloudAvailable = data?.has_cloud_saves === true;
    const hasLocal = !!data?.has_local_saves;
    const hasCloud = cloudAvailable || !!remote;
    const localTs = local.timestamp;
    const remoteTs = remote?.timestamp;
    const case1 = hasCloud && !hasLocal;
    // Treat local vs cloud as "in sync" within a small tolerance: gogdl stores
    // the cloud mtime (X-Object-Meta-LocalLastModified) truncated to whole
    // seconds, while the local file's st_mtime keeps sub-second precision — so a
    // freshly-pushed save reads ~0.x s apart and must NOT breathe. A genuine new
    // save is always many seconds newer, so 2 s never masks a real divergence.
    const case2and3 = hasCloud &&
        hasLocal &&
        !!localTs &&
        !!remoteTs &&
        Math.abs(localTs - remoteTs) > TS_SYNC_TOLERANCE_S;
    const case4 = !hasCloud && hasLocal;
    const shouldBreathe = !noCloudSupport && !syncing && (case1 || case2and3 || case4);
    let icon = SP_JSX.jsx(FaCloud, {});
    let label = t("play.cloudSave.label");
    let tintRing;
    if (syncing) {
        icon = SP_JSX.jsx(FaSyncAlt, { className: "unifideck-cloud-spin" });
        label = t("play.cloudSave.statusSyncing");
        tintRing = "rgba(26, 159, 255, 0.55)";
    }
    else if (noCloudSupport) {
        label = t("play.cloudSave.noCloudSupport");
    }
    else if (unresolved) {
        label = t("play.cloudSave.statusUnresolved");
        tintRing = "rgba(244, 180, 0, 0.55)";
    }
    else if (shouldBreathe) {
        icon = SP_JSX.jsx(FaCloudDownloadAlt, {});
        label = cloudAvailable
            ? t("play.cloudSave.statusCloudAvailable")
            : t("play.cloudSave.label");
    }
    const style = {
        ...iconBtnStyle,
        ...({}),
        ...(tintRing ? { boxShadow: `0 0 0 2px ${tintRing} inset` } : {}),
        ...(noCloudSupport ? { opacity: 0.5 } : {}),
    };
    const open = () => {
        if (!data)
            return;
        DFL.showModal(SP_JSX.jsx(CloudSaveModal, { store: store, gameId: gameId, gameTitle: gameTitle, initialStatus: data, closeModal: () => { } }));
    };
    return (SP_JSX.jsx(DFL.DialogButton, { className: `unifideck-icon-btn${shouldBreathe ? " unifideck-breathe" : ""}`, style: style, disabled: loading || syncing || noCloudSupport, onClick: open, "aria-label": label, children: icon }));
};

const defaultBridge$1 = new SteamBridge();
const RUNNING_POLL_MS = 2000;
const STEAM_STATUS_RUNNING = 4;
const STEAM_STATUS_LAUNCHING = 1;
function readDisplayStatus(appId) {
    const store = window.appStore;
    const app = store?.m_mapApps?.get?.(appId);
    return app?.local_per_client_data?.display_status;
}
function openControllerConfig$1(appId) {
    window.SteamClient?.Apps?.ShowControllerConfigurator?.(appId);
}
function openAppSettings$1(appId) {
    window.SteamClient?.Apps?.OpenAppSettingsDialog?.(appId, "general");
}
const InstalledButtons = ({ appId, bridge = defaultBridge$1, }) => {
    const { t } = useTranslation();
    const { data: game, loading } = useGameInfo(appId);
    const actions = useGameActions(bridge);
    const toast = useToast();
    const [isRunning, setIsRunning] = SP_REACT.useState(false);
    const [hasUpdate, setHasUpdate] = SP_REACT.useState(false);
    // NOTE: we deliberately do NOT touch Steam's Force-Compatibility here.
    // This used to capture it into proton_settings.json and clear it so
    // RunGame wouldn't wrap our launcher in Proton — but clearing it meant
    // the launcher could never read the user's ACTUAL selection at launch
    // time (only a copy that went stale whenever the capture/restore dance
    // was interrupted), so switching Proton in Steam's dialog appeared to do
    // nothing for some games and work for others purely by timing.
    // ``config.vdf``'s CompatToolMapping is now the single source of truth,
    // read by ``selector.select_proton_version``; the double-Proton problem
    // the clearing existed to avoid is handled properly at the umu spawn
    // point by ``launcher.proton.infrastructure.container_escape``.
    // Running-state poll (2 s).
    SP_REACT.useEffect(() => {
        let cancelled = false;
        const tick = () => {
            if (cancelled)
                return;
            const status = readDisplayStatus(appId);
            if (status === undefined)
                return;
            setIsRunning(status === STEAM_STATUS_RUNNING || status === STEAM_STATUS_LAUNCHING);
        };
        tick();
        const id = window.setInterval(tick, RUNNING_POLL_MS);
        return () => {
            cancelled = true;
            window.clearInterval(id);
        };
    }, [appId]);
    // Update check — one-shot on mount. The backend RPC takes
    // (store, game_id) and returns { has_update } — passing the raw
    // appId (the old call) threw "missing argument: game_id" on every
    // page load and update detection never worked.
    SP_REACT.useEffect(() => {
        if (!game)
            return;
        let cancelled = false;
        call("check_game_update", game.store, game.id)
            .then((res) => {
            if (cancelled)
                return;
            setHasUpdate(Boolean(res?.has_update));
        })
            .catch(() => {
            /* non-critical */
        });
        return () => {
            cancelled = true;
        };
    }, [game]);
    const onPlay = SP_REACT.useCallback(() => {
        if (!game)
            return;
        actions.launch(appId);
    }, [actions, appId, game]);
    const onStop = SP_REACT.useCallback(() => {
        actions.terminate(appId);
    }, [actions, appId]);
    const onUpdate = SP_REACT.useCallback(async () => {
        if (!game)
            return;
        try {
            const res = await call("update_game", appId);
            if (res?.success) {
                setHasUpdate(false);
                toast.success(t("toasts.updateQueued"));
            }
            else {
                toast.error(t("toasts.updateFailed"), res?.error ?? "");
            }
        }
        catch (e) {
            toast.error(t("toasts.updateFailed"), String(e));
        }
    }, [appId, game, t, toast]);
    const onUninstall = SP_REACT.useCallback(() => {
        if (!game)
            return;
        DFL.showModal(SP_JSX.jsx(UninstallConfirmModal, { gameId: appId, gameTitle: game.title, onConfirm: async (deletePrefix) => {
                const r = await actions.uninstall(appId, deletePrefix);
                if (r?.success)
                    toast.success(t("toasts.uninstallDone"));
            }, closeModal: () => { } }));
    }, [actions, appId, game, t, toast]);
    const primaryButtons = (() => {
        if (isRunning) {
            return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsxs(DFL.DialogButton, { className: "unifideck-resume-btn", disabled: loading, onClick: onPlay, style: actionBtnStyle$1, children: [SP_JSX.jsx(FaPlay, {}), " ", t("play.resume")] }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-stop-btn", onClick: onStop, style: iconBtnStyle, "aria-label": t("play.stop"), children: SP_JSX.jsx(FaTimes, {}) })] }));
        }
        if (hasUpdate) {
            return (SP_JSX.jsxs(DFL.DialogButton, { className: "unifideck-update-btn", disabled: loading || actions.isWorking, onClick: onUpdate, style: actionBtnStyle$1, children: [SP_JSX.jsx(FaSyncAlt, {}), " ", t("play.update")] }));
        }
        return (SP_JSX.jsxs(DFL.DialogButton, { className: "unifideck-play-btn", disabled: loading, onClick: onPlay, style: actionBtnStyle$1, children: [SP_JSX.jsx(FaPlay, {}), " ", t("play.play")] }));
    })();
    return (
    // autoFocus is intentional: claims gamepad focus for the primary action
    // eslint-disable-next-line jsx-a11y/no-autofocus
    SP_JSX.jsxs(PlayShell, { autoFocus: true, children: [primaryButtons, SP_JSX.jsx(MetaInline, { sizeBytes: game?.size_bytes, showLastPlayed: true, appId: appId, store: game?.store, gameId: game?.id, installed: true }), SP_JSX.jsxs(IconGroup, { children: [game && (SP_JSX.jsx(CloudSaveButton, { store: game.store, gameId: game.id, gameTitle: game.title })), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-icon-btn", style: iconBtnStyle, onClick: () => openControllerConfig$1(appId), "aria-label": t("playButton.controllerConfig"), children: SP_JSX.jsx(FaGamepad, {}) }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-icon-btn", style: iconBtnStyle, onClick: (e) => {
                            // Open Steam's native app menu (Manage / Properties / …),
                            // matching the native gear; fall back to Properties directly.
                            if (!openNativeAppManageMenu(e?.currentTarget)) {
                                openAppSettings$1(appId);
                            }
                        }, "aria-label": t("playButton.appSettings"), children: SP_JSX.jsx(FaCog, {}) }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-icon-btn", style: iconBtnStyle, disabled: loading || actions.isWorking, onClick: onUninstall, "aria-label": t("play.uninstall"), children: SP_JSX.jsx(FaTrash, {}) })] })] }));
};

function openControllerConfig(appId) {
    window.SteamClient?.Apps?.ShowControllerConfigurator?.(appId);
}
function openAppSettings(appId) {
    window.SteamClient?.Apps?.OpenAppSettingsDialog?.(appId, "general");
}
const XCloudButtons = ({ appId, gameId }) => {
    const { t } = useTranslation();
    const onPlay = SP_REACT.useCallback(async () => {
        const launched = await launchAppWithConfiguredGamepad(appId);
        if (!launched) {
            // Steam Apps surface unavailable — open the stream directly.
            window.open(`steam://openurl/https://www.xbox.com/play/launch/${gameId}`, "_blank");
        }
    }, [appId, gameId]);
    return (
    // autoFocus is intentional: claims gamepad focus for the primary action
    // eslint-disable-next-line jsx-a11y/no-autofocus
    SP_JSX.jsxs(PlayShell, { autoFocus: true, children: [SP_JSX.jsxs(DFL.DialogButton, { className: "unifideck-play-btn", onClick: onPlay, style: actionBtnStyle$1, children: [SP_JSX.jsx(FaCloud, {}), " ", t("play.playOnCloud", "Play on Cloud")] }), SP_JSX.jsx(MetaInline, { showLastPlayed: true, appId: appId }), SP_JSX.jsxs(IconGroup, { children: [SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-icon-btn", style: iconBtnStyle, onClick: () => openControllerConfig(appId), "aria-label": t("playButton.controllerConfig"), children: SP_JSX.jsx(FaGamepad, {}) }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-icon-btn", style: iconBtnStyle, onClick: (e) => {
                            // Open Steam's native app menu (Manage / Properties / …),
                            // matching the native gear; fall back to Properties directly.
                            if (!openNativeAppManageMenu(e?.currentTarget)) {
                                openAppSettings(appId);
                            }
                        }, "aria-label": t("playButton.appSettings"), children: SP_JSX.jsx(FaCog, {}) })] })] }));
};

/**
 * Top-level wrapper rendered in place of Steam's own Play section
 * for Unifideck games. Picks the right variant (Not-installed /
 * Downloading / Installed / XCloud) based on `usePlaySection`, or a
 * loading skeleton while the game info resolves.
 */
const PlaySectionWrapper = ({ appId }) => {
    const state = usePlaySection(appId);
    SP_REACT.useEffect(() => {
        injectPlayFocusStyles();
    }, []);
    let body;
    if (!state.shouldOverride) {
        // Info still resolving for this shortcut (its store is never
        // "steam", so this is the only reason shouldOverride is false).
        body = SP_JSX.jsx(PlayLoadingSkeleton, {});
    }
    else {
        switch (state.kind) {
            case "not-installed":
                body = SP_JSX.jsx(NotInstalledButtons, { appId: appId });
                break;
            case "downloading":
                body = SP_JSX.jsx(DownloadingButtons, { download: state.download });
                break;
            case "installed":
                body = SP_JSX.jsx(InstalledButtons, { appId: state.appId });
                break;
            case "xcloud":
                body = SP_JSX.jsx(XCloudButtons, { appId: state.appId, gameId: state.gameId });
                break;
            default:
                body = SP_JSX.jsx(PlayLoadingSkeleton, {});
        }
    }
    return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("style", { children: nativeAppDetailsHideCss() + HLTB_CLEAN_BOX_POSITION_CSS }), body] }));
};

/**
 * useGameMetadata — per-appId display metadata with TTL cache.
 *
 * Companion to {@link useGameInfo}. Where `useGameInfo` returns
 * install state (size, install_path, is_installed, deck_rating),
 * this hook returns the rich Steam-Store-derived display payload
 * (developer, publisher, release date, description, genres,
 * deck compatibility category, Metacritic score, …).
 *
 * Cache invariants :
 *  - TTL = 60_000ms — metadata changes far less often than install
 *    state, so we hold it 12× longer than the {@link useGameInfo}
 *    cache to avoid re-RPCing on every panel open.
 *  - Concurrent consumers of the same appId share one in-flight
 *    fetch (de-duplication via the `inflight` promise slot).
 *  - `refresh()` always re-fetches, bypassing the TTL gate.
 */

const CACHE_TTL = 60000;
const cache = new Map();
/**
 * Fetch + cache the panel's rich display metadata for one
 * shortcut. Returns ``data === null`` until the first fetch
 * resolves; consumers should not block panel render on it
 * (loading is a soft hint).
 *
 * @param appId — Steam shortcut app-id, or null for "no game".
 */
function useGameMetadata(appId) {
    const fetch = useRPC(rpcRoutes.getGameMetadataDisplay);
    const [state, setState] = SP_REACT.useState(() => {
        if (appId == null)
            return { data: null, loading: false, error: null };
        const cached = cache.get(appId);
        return {
            data: cached?.data ?? null,
            loading: cached?.data == null,
            error: null,
        };
    });
    const load = SP_REACT.useCallback(async (force) => {
        if (appId == null) {
            setState({ data: null, loading: false, error: null });
            return;
        }
        const cached = cache.get(appId);
        if (!force && cached && Date.now() - cached.ts < CACHE_TTL) {
            setState({ data: cached.data, loading: false, error: null });
            return;
        }
        if (cached?.inflight && !force) {
            const data = await cached.inflight;
            setState({ data, loading: false, error: null });
            return;
        }
        setState((s) => ({
            data: s.data ?? cached?.data ?? null,
            loading: s.data == null && cached?.data == null,
            error: null,
        }));
        const promise = fetch(appId).then((data) => {
            cache.set(appId, { data, ts: Date.now(), inflight: null });
            return data;
        }, (err) => {
            cache.set(appId, { data: null, ts: Date.now(), inflight: null });
            throw err;
        });
        cache.set(appId, {
            data: cached?.data ?? null,
            ts: cached?.ts ?? 0,
            inflight: promise,
        });
        try {
            const data = await promise;
            setState({ data, loading: false, error: null });
        }
        catch (err) {
            setState({ data: null, loading: false, error: err });
        }
    }, [appId, fetch]);
    SP_REACT.useEffect(() => {
        void load(false);
    }, [load]);
    const refresh = SP_REACT.useCallback(() => load(true), [load]);
    return { ...state, refresh };
}

const COMPAT_COLORS$1 = {
    3: { bg: "#59bf40", fg: "#ffffff" },
    2: { bg: "#ffc82c", fg: "#000000" },
    1: { bg: "#ff4444", fg: "#ffffff" },
    0: { bg: "#666666", fg: "#ffffff" },
};
const COMPAT_LABEL = {
    3: "verified",
    2: "playable",
    1: "unsupported",
    0: "unknown",
};
const GameInfoDetailsModal = ({ meta, closeModal }) => {
    const { t } = useTranslation();
    const colors = COMPAT_COLORS$1[meta.deck_compatibility];
    const labelKey = `gameInfoPanel.compatibility.${COMPAT_LABEL[meta.deck_compatibility]}`;
    const protonDbUrl = meta.steam_app_id
        ? `https://www.protondb.com/app/${meta.steam_app_id}`
        : null;
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("gameInfoPanel.compatibility.modalTitle"), strDescription: "", bHideCloseIcon: false, onOK: closeModal, onCancel: closeModal, strOKButtonText: t("gameInfoPanel.compatibility.close"), children: SP_JSX.jsxs("div", { style: { padding: "10px 0" }, children: [SP_JSX.jsxs("div", { style: {
                        marginBottom: 16,
                        display: "flex",
                        gap: 12,
                        alignItems: "center",
                    }, children: [SP_JSX.jsx("span", { style: {
                                padding: "4px 12px",
                                borderRadius: 4,
                                background: colors.bg,
                                color: colors.fg,
                                fontWeight: 700,
                                fontSize: 12,
                                letterSpacing: 0.5,
                                textTransform: "uppercase",
                            }, children: t(labelKey) }), meta.title && (SP_JSX.jsx("span", { style: { color: "#c7d5e0", fontSize: 14, fontWeight: 600 }, children: meta.title }))] }), meta.deck_test_results.length > 0 ? (SP_JSX.jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: meta.deck_test_results.map((r, i) => (SP_JSX.jsxs("li", { style: {
                            display: "flex",
                            alignItems: "center",
                            gap: 8,
                            padding: "6px 0",
                            fontSize: 13,
                            color: "#acb2b8",
                        }, children: [r.passed ? (SP_JSX.jsx(FaCheckCircle, { style: { color: "#59bf40", flexShrink: 0 } })) : (SP_JSX.jsx(FaExclamationTriangle, { style: { color: "#ffc82c", flexShrink: 0 } })), SP_JSX.jsx("span", { children: r.text })] }, i))) })) : (SP_JSX.jsx("div", { style: { color: "#8f98a0", fontSize: 13, fontStyle: "italic" }, children: t("gameInfoPanel.noTestResults") })), protonDbUrl && (SP_JSX.jsx("div", { style: { marginTop: 16 }, children: SP_JSX.jsx(DFL.DialogButton, { style: {
                            width: "auto",
                            flex: "0 0 auto",
                            display: "inline-flex",
                            alignItems: "center",
                            minWidth: 0,
                        }, onClick: () => {
                            window.open(protonDbUrl, "_blank", "width=1024,height=768,popup=yes");
                            closeModal();
                        }, children: SP_JSX.jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx(FaExternalLinkAlt, {}), t("gameInfoPanel.compatibility.viewOnProtonDb")] }) }) }))] }) }));
};

/**
 * Achievement hooks.
 *
 * `useAchievements` — loads a game's achievements (definitions + this user's
 *   unlock status) on mount and exposes a `refresh` that bypasses the backend
 *   TTL cache. Backed by `get_game_achievements` (may hit the network), so it's
 *   used on demand (modal open), never on the render hot path.
 *
 * `useLastSessionAchievements` — a fast, network-free read of the watcher's
 *   persisted last-session summary (`get_last_session_achievements`), for the
 *   game-info panel's "last session" row.
 */

/** Load + cache a game's achievements; `enabled=false` defers the fetch. */
function useAchievements(store, gameId, enabled = true) {
    const rpc = useRPC(rpcRoutes.getGameAchievements);
    const [data, setData] = SP_REACT.useState(null);
    const [error, setError] = SP_REACT.useState(null);
    const [loading, setLoading] = SP_REACT.useState(enabled);
    const cancelledRef = SP_REACT.useRef(false);
    const load = SP_REACT.useCallback(async (force) => {
        setLoading(true);
        setError(null);
        try {
            const result = await rpc(store, gameId, force);
            if (!cancelledRef.current)
                setData(result);
        }
        catch (e) {
            if (!cancelledRef.current)
                setError(e);
        }
        finally {
            if (!cancelledRef.current)
                setLoading(false);
        }
    }, [rpc, store, gameId]);
    SP_REACT.useEffect(() => {
        cancelledRef.current = false;
        if (enabled)
            void load(false);
        return () => {
            cancelledRef.current = true;
        };
    }, [enabled, load]);
    const refresh = SP_REACT.useCallback(() => load(true), [load]);
    return { data, loading, error, refresh };
}
/** Last play session's unlock summary, or null. Fast, network-free read. */
function useLastSessionAchievements(store, gameId, enabled = true) {
    const { data } = useRPCQuery(rpcRoutes.getLastSessionAchievements, [store, gameId], { enabled });
    return data ?? null;
}

/** Map a backend RpcError code (carried as the error message) to a body key. */
function errorKey(code) {
    switch (code) {
        case "not_authed":
        case "auth_expired":
            return "achievementsModal.errorAuth";
        case "offline":
            return "achievementsModal.errorOffline";
        case "no_client_id":
        case "achievements_unsupported":
            return "achievementsModal.errorUnavailable";
        default:
            return "achievementsModal.error";
    }
}
const TILE_BASE = {
    display: "flex",
    gap: 10,
    padding: 8,
    borderRadius: 6,
    background: "rgba(0,0,0,0.25)",
    alignItems: "center",
};
const AchievementTile = ({ ach }) => {
    const { t } = useTranslation();
    const maskedHidden = ach.hidden && !ach.unlocked;
    const name = maskedHidden ? t("achievementsModal.hidden") : ach.name;
    const img = ach.unlocked ? ach.image_unlocked : ach.image_locked;
    return (SP_JSX.jsxs(DFL.Focusable
    // A Focusable leaf (onActivate makes it a real focus target, not a
    // pass-through) so the controller can step through every achievement;
    // each focus scrolls the row into the modal's scroll viewport.
    , { 
        // A Focusable leaf (onActivate makes it a real focus target, not a
        // pass-through) so the controller can step through every achievement;
        // each focus scrolls the row into the modal's scroll viewport.
        onActivate: () => { }, onFocus: (e) => e.currentTarget.scrollIntoView({ block: "nearest" }), style: { ...TILE_BASE, opacity: ach.unlocked ? 1 : 0.55 }, children: [SP_JSX.jsxs("div", { style: { position: "relative", flexShrink: 0 }, children: [img ? (SP_JSX.jsx("img", { src: img, alt: "", style: {
                            width: 48,
                            height: 48,
                            borderRadius: 4,
                            filter: ach.unlocked ? "none" : "grayscale(1)",
                        } })) : (SP_JSX.jsx("div", { style: {
                            width: 48,
                            height: 48,
                            borderRadius: 4,
                            background: "#2a2e33",
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                        }, children: SP_JSX.jsx(FaTrophy, { style: { color: ach.unlocked ? "#ffc82c" : "#5a6066" } }) })), !ach.unlocked && (SP_JSX.jsx(FaLock, { style: {
                            position: "absolute",
                            right: -2,
                            bottom: -2,
                            color: "#8f98a0",
                            fontSize: 12,
                        } }))] }), SP_JSX.jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [SP_JSX.jsx("div", { style: {
                            fontSize: 13,
                            fontWeight: 600,
                            color: ach.unlocked ? "#ffffff" : "#acb2b8",
                        }, children: name }), !maskedHidden && ach.description && (SP_JSX.jsx("div", { style: {
                            fontSize: 11,
                            color: "#8f98a0",
                            overflow: "hidden",
                            textOverflow: "ellipsis",
                            display: "-webkit-box",
                            WebkitLineClamp: 2,
                            WebkitBoxOrient: "vertical",
                        }, children: ach.description })), ach.unlocked && ach.unlocked_at && (SP_JSX.jsx("div", { style: { fontSize: 11, color: "#59bf40", marginTop: 2 }, children: t("achievementsModal.unlockedOn", {
                            date: new Date(ach.unlocked_at * 1000).toLocaleDateString(),
                        }) }))] })] }));
};
const GameAchievementsModal = ({ store, gameId, title, closeModal, }) => {
    const { t } = useTranslation();
    const { data, loading, error, refresh } = useAchievements(store, gameId);
    const percent = data ? Math.round(data.percent) : 0;
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: title || t("achievementsModal.title"), strDescription: "", bHideCloseIcon: false, onOK: closeModal, onCancel: closeModal, strOKButtonText: t("achievementsModal.close"), children: SP_JSX.jsxs(DFL.Focusable, { "flow-children": "column", style: { padding: "6px 0" }, children: [data && data.total > 0 && (SP_JSX.jsxs("div", { style: { marginBottom: 12 }, children: [SP_JSX.jsxs("div", { style: {
                                display: "flex",
                                justifyContent: "space-between",
                                alignItems: "center",
                                marginBottom: 6,
                            }, children: [SP_JSX.jsx("span", { style: { fontSize: 14, fontWeight: 600, color: "#c7d5e0" }, children: t("achievementsModal.progress", {
                                        unlocked: data.unlocked,
                                        total: data.total,
                                        percent,
                                    }) }), SP_JSX.jsxs(DFL.DialogButton, { style: {
                                        width: "auto",
                                        flex: "0 0 auto",
                                        minWidth: 0,
                                        padding: "4px 12px",
                                        display: "inline-flex",
                                        alignItems: "center",
                                        gap: 6,
                                    }, disabled: loading, onClick: () => void refresh(), children: [SP_JSX.jsx(FaSyncAlt, {}), t("achievementsModal.refresh")] })] }), SP_JSX.jsx("div", { style: {
                                height: 6,
                                borderRadius: 3,
                                background: "rgba(255,255,255,0.12)",
                                overflow: "hidden",
                            }, children: SP_JSX.jsx("div", { style: {
                                    width: `${percent}%`,
                                    height: "100%",
                                    background: "#59bf40",
                                } }) })] })), loading && !data && (SP_JSX.jsx("div", { style: { color: "#8f98a0", fontSize: 13, padding: "12px 0" }, children: t("achievementsModal.loading") })), error && (SP_JSX.jsx("div", { style: { color: "#ffc82c", fontSize: 13, padding: "12px 0" }, children: t(errorKey(error.message)) })), data && data.total === 0 && !loading && (SP_JSX.jsx("div", { style: {
                        color: "#8f98a0",
                        fontSize: 13,
                        fontStyle: "italic",
                        padding: "12px 0",
                    }, children: t("achievementsModal.empty") })), data && data.total > 0 && (SP_JSX.jsx(DFL.Focusable, { "flow-children": "column", style: {
                        display: "flex",
                        flexDirection: "column",
                        gap: 6,
                        maxHeight: 360,
                        overflowY: "auto",
                    }, children: data.achievements.map((ach) => (SP_JSX.jsx(AchievementTile, { ach: ach }, ach.key))) }))] }) }));
};

const COMPAT_COLORS = {
    3: { bg: "#59bf40", fg: "#ffffff" },
    2: { bg: "#ffc82c", fg: "#000000" },
    1: { bg: "#ff4444", fg: "#ffffff" },
    0: { bg: "#666666", fg: "#ffffff" },
};
const COMPAT_LABEL_KEY = {
    3: "gameInfoPanel.compatibility.verified",
    2: "gameInfoPanel.compatibility.playable",
    1: "gameInfoPanel.compatibility.unsupported",
    0: "gameInfoPanel.compatibility.unknown",
};
/** Inline-flex sizing for every DialogButton in the panel.
 *  @decky/ui's DialogButton defaults to ``width: 100%`` (settings-
 *  menu styling) — without ``flex: 0 0 auto`` the buttons stretch
 *  to fill their parent flex line. */
const buttonStyle$1 = {
    padding: "4px 12px",
    fontSize: 12,
    minWidth: 0,
    width: "auto",
    flex: "0 0 auto",
    display: "inline-flex",
    alignItems: "center",
};
const defaultBridge = new SteamBridge();
const GameInfoCompatRow = ({ appId, meta, synopsisOpen, onToggleSynopsis, bridge = defaultBridge, }) => {
    const { t } = useTranslation();
    const { data: game } = useGameInfo(appId);
    const downloads = useDownloads();
    const actions = useGameActions(bridge);
    const toast = useToast();
    const activeDownload = SP_REACT.useMemo(() => {
        if (!game || !downloads.queue)
            return null;
        const all = [downloads.queue.current, ...downloads.queue.queued];
        return (all.find((d) => d != null && d.store === game.store && d.game_id === game.id) ?? null);
    }, [downloads.queue, game]);
    const isDownloading = activeDownload != null;
    const showUninstallConfirm = SP_REACT.useCallback(() => {
        if (!game)
            return;
        DFL.showModal(SP_JSX.jsx(UninstallConfirmModal, { gameId: appId, gameTitle: game.title, onConfirm: async (deletePrefix) => {
                const r = await actions.uninstall(appId, deletePrefix);
                if (r?.success)
                    toast.success(t("toasts.uninstallComplete"));
            }, closeModal: () => { } }));
    }, [actions, appId, game, t, toast]);
    const onDetails = SP_REACT.useCallback(() => {
        DFL.showModal(SP_JSX.jsx(GameInfoDetailsModal, { meta: meta, closeModal: () => { } }));
    }, [meta]);
    const onAchievements = SP_REACT.useCallback(() => {
        if (!game)
            return;
        DFL.showModal(SP_JSX.jsx(GameAchievementsModal, { store: game.store, gameId: game.id, title: game.title, closeModal: () => { } }));
    }, [game]);
    // The compat row only exposes Uninstall — Install / Cancel are
    // already covered by the main play section directly above. Show
    // it only when the game is installed and not mid-(re)download,
    // preserving the Microsoft "not_compatible" exclusion.
    const showAction = !!game &&
        Boolean(game.is_installed) &&
        !isDownloading &&
        !(game.store === "microsoft" &&
            (game.store_tags ?? []).includes("not_compatible"));
    const compatColors = COMPAT_COLORS[meta.deck_compatibility];
    return (SP_JSX.jsxs(DFL.Focusable, { "flow-children": "row", onFocus: (e) => {
            e.currentTarget.scrollIntoView({ behavior: "smooth", block: "center" });
        }, style: {
            display: "flex",
            flexDirection: "column",
            gap: 8,
        }, children: [SP_JSX.jsxs("div", { style: {
                    display: "flex",
                    flexWrap: "wrap",
                    alignItems: "center",
                    gap: 8,
                }, children: [SP_JSX.jsx("span", { style: {
                            padding: "4px 12px",
                            borderRadius: 4,
                            background: compatColors.bg,
                            color: compatColors.fg,
                            fontWeight: 700,
                            fontSize: 12,
                            letterSpacing: 0.5,
                            textTransform: "uppercase",
                        }, children: t(COMPAT_LABEL_KEY[meta.deck_compatibility]) }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle$1, onClick: onDetails, children: t("gameInfoPanel.buttons.details") }), meta.description && (SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: {
                            ...buttonStyle$1,
                            ...(synopsisOpen
                                ? { background: "#1a9fff", color: "#ffffff" }
                                : null),
                        }, onClick: onToggleSynopsis, children: t("gameInfoPanel.buttons.synopsis") })), (game?.store === "gog" || game?.store === "epic") && (SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle$1, onClick: onAchievements, children: t("gameInfoPanel.buttons.achievements") })), showAction && (SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button unifideck-install-button uninstall-state", style: buttonStyle$1, disabled: actions.isWorking, onClick: showUninstallConfirm, children: t("gameInfoPanel.buttons.uninstall") }))] }), meta.genres.length > 0 && (SP_JSX.jsx("div", { style: { color: "#8f98a0", fontSize: 13 }, children: meta.genres.join(" • ") }))] }));
};

function formatSize(bytes) {
    if (!bytes)
        return null;
    const gb = bytes / 1024 ** 3;
    if (gb >= 1)
        return `${gb.toFixed(1)} GB`;
    const mb = bytes / 1024 ** 2;
    return `${mb.toFixed(0)} MB`;
}
function metacriticColor(score) {
    if (score >= 75)
        return "#66cc33";
    if (score >= 50)
        return "#ffcc33";
    return "#ff0000";
}
const Cell = ({ label, children, }) => (SP_JSX.jsxs("span", { style: {
        display: "inline-flex",
        alignItems: "center",
        gap: 6,
        fontSize: 13,
    }, children: [SP_JSX.jsx("strong", { style: { color: "#8f98a0", fontWeight: 600 }, children: label }), SP_JSX.jsx("span", { style: { color: "#c7d5e0" }, children: children })] }));
const GameInfoInfoRow = ({ appId, game, meta }) => {
    const { t } = useTranslation();
    // Fetched out-of-band so it never blocks the panel; fall back to any
    // size already on the game record. See useGameSize.
    const fetchedSize = useGameSize(appId, Boolean(game.is_installed));
    const size = formatSize(fetchedSize && fetchedSize > 0 ? fetchedSize : game.size_bytes);
    return (SP_JSX.jsxs(DFL.Focusable, { "flow-children": "row", onActivate: () => { }, noFocusRing: true, className: "unifideck-game-info-row", style: {
            display: "flex",
            flexWrap: "wrap",
            alignItems: "center",
            gap: 16,
            background: "rgba(0, 0, 0, 0.3)",
            borderRadius: 6,
            padding: "12px 16px",
        }, onFocus: (e) => {
            e.currentTarget.scrollIntoView({ behavior: "smooth", block: "center" });
        }, children: [SP_JSX.jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [SP_JSX.jsx(StoreIcon, { store: game.store, size: 20 }), SP_JSX.jsx("span", { style: { fontWeight: 600, color: "#c7d5e0", fontSize: 14 }, children: game.title })] }), size && SP_JSX.jsx(Cell, { label: t("gameInfoPanel.labels.size"), children: size }), meta.developer && (SP_JSX.jsx(Cell, { label: t("gameInfoPanel.labels.developer"), children: meta.developer })), meta.publisher && (SP_JSX.jsx(Cell, { label: t("gameInfoPanel.labels.publisher"), children: meta.publisher })), meta.release_date && (SP_JSX.jsx(Cell, { label: t("gameInfoPanel.labels.released"), children: meta.release_date })), meta.metacritic != null && (SP_JSX.jsx(Cell, { label: t("gameInfoPanel.labels.metacritic"), children: SP_JSX.jsx("span", { style: {
                        padding: "2px 8px",
                        borderRadius: 3,
                        background: metacriticColor(meta.metacritic),
                        color: "#000000",
                        fontWeight: 700,
                        fontSize: 12,
                    }, children: meta.metacritic }) }))] }));
};

const GameInfoSynopsisSection = ({ description }) => {
    return (SP_JSX.jsx(DFL.Focusable, { "flow-children": "column", onActivate: () => { }, noFocusRing: true, className: "unifideck-synopsis-section", style: {
            background: "rgba(0, 0, 0, 0.3)",
            borderRadius: 6,
            padding: "12px 16px",
            fontSize: 14,
            color: "#acb2b8",
            lineHeight: 1.5,
            whiteSpace: "pre-wrap",
        }, onFocus: (e) => {
            e.currentTarget.scrollIntoView({ behavior: "smooth", block: "center" });
        }, children: description }));
};

const SUPPORT_URLS = {
    epic: "https://www.epicgames.com/help/assistant",
    gog: "https://support.gog.com/hc/en-us?product=gog",
    amazon: "https://www.amazon.in/gp/help/customer/display.html?nodeId=GA5ZHN5T2JX8UGF7",
    ubisoft: "https://www.ubisoft.com/en-us/help",
    ea: "https://help.ea.com/en/",
    battlenet: "https://us.battle.net/support/en/",
    itch: "https://itch.io/support",
    microsoft: "https://support.xbox.com/en-US/help/games-apps/game-setup-and-play/troubleshoot-pc-games",
    steam: "https://help.steampowered.com",
};
function openUrl(url) {
    window.open(url, "_blank", "width=1024,height=768,popup=yes");
}
function getSupportUrl(store, steamAppId) {
    if (SUPPORT_URLS[store])
        return SUPPORT_URLS[store];
    return steamAppId
        ? `https://help.steampowered.com/en/wizard/HelpWithGame/?appid=${steamAppId}`
        : SUPPORT_URLS.steam;
}
const buttonStyle = {
    padding: "4px 12px",
    fontSize: 12,
    minWidth: 0,
    width: "auto",
    flex: "0 0 auto",
    display: "inline-flex",
    alignItems: "center",
};
const GameInfoNavButtons = ({ meta }) => {
    const { t } = useTranslation();
    const steamId = meta.steam_app_id;
    const hasSteam = meta.has_steam_store_page && steamId > 0;
    const storePageUrl = hasSteam ? `steam://store/${steamId}` : meta.store_url;
    return (SP_JSX.jsxs(DFL.Focusable, { "flow-children": "row", onFocus: (e) => {
            e.currentTarget.scrollIntoView({ behavior: "smooth", block: "center" });
        }, style: {
            display: "flex",
            flexWrap: "wrap",
            gap: 8,
            alignItems: "center",
        }, children: [storePageUrl && (SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle, onClick: () => openUrl(storePageUrl), children: t("gameInfoPanel.buttons.storePage") })), hasSteam && (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle, onClick: () => openUrl(`steam://openurl/https://store.steampowered.com/dlc/${steamId}`), children: t("gameInfoPanel.buttons.dlc") }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle, onClick: () => openUrl(`steam://url/GameHub/${steamId}`), children: t("gameInfoPanel.buttons.communityHub") }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle, onClick: () => openUrl(`steam://openurl/https://store.steampowered.com/points/shop/app/${steamId}`), children: t("gameInfoPanel.buttons.pointsShop") }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle, onClick: () => openUrl(`steam://openurl/https://steamcommunity.com/app/${steamId}/discussions/`), children: t("gameInfoPanel.buttons.discussions") }), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle, onClick: () => openUrl(`steam://openurl/https://steamcommunity.com/app/${steamId}/guides/`), children: t("gameInfoPanel.buttons.guides") })] })), SP_JSX.jsx(DFL.DialogButton, { className: "unifideck-nav-button", style: buttonStyle, onClick: () => openUrl(getSupportUrl(meta.store, steamId)), children: t("gameInfoPanel.buttons.support") })] }));
};

const GameAchievementsSummary = ({ store, gameId, title, }) => {
    const { t } = useTranslation();
    const summary = useLastSessionAchievements(store, gameId);
    const open = SP_REACT.useCallback(() => {
        DFL.showModal(SP_JSX.jsx(GameAchievementsModal, { store: store, gameId: gameId, title: title, closeModal: () => { } }));
    }, [store, gameId, title]);
    if (!summary || summary.unlocked <= 0)
        return null;
    const shown = summary.names.slice(0, 3).filter(Boolean);
    const extra = summary.unlocked - shown.length;
    const names = shown.join(", ") + (extra > 0 ? ` (+${extra})` : "");
    return (SP_JSX.jsxs(DFL.Focusable, { className: "unifideck-nav-button", onActivate: open, onFocus: (e) => e.currentTarget.scrollIntoView({ behavior: "smooth", block: "center" }), style: {
            display: "flex",
            alignItems: "center",
            gap: 8,
            padding: "8px 12px",
            borderRadius: 6,
            background: "rgba(0,0,0,0.25)",
            fontSize: 13,
            color: "#c7d5e0",
        }, children: [SP_JSX.jsx(FaTrophy, { style: { color: "#ffc82c", flexShrink: 0 } }), SP_JSX.jsx("span", { children: t("gameInfoPanel.lastSession", { names }) })] }));
};

/** Focus-state CSS shared by every interactive element in the
 *  panel. Injected once at panel mount because the panel is
 *  rendered into Steam's spliced React tree — there is no app-
 *  level <head> we can collocate this with. */
const PANEL_FOCUS_CSS = `
@keyframes unifideck-focus-breathe {
  0%, 100% { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.35); }
  50%      { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7); }
}
.unifideck-nav-button.gpfocus,
.unifideck-nav-button:hover {
  animation: unifideck-focus-breathe 1.5s ease-in-out infinite;
}
.unifideck-install-button.install-state   { background-color: #1a9fff; }
.unifideck-install-button.uninstall-state { background-color: #d32f2f; }
.unifideck-install-button.cancel-state    { background-color: #d32f2f; }
.unifideck-game-info-row.gpfocus,
.unifideck-synopsis-section.gpfocus {
  animation: unifideck-focus-breathe 1.5s ease-in-out infinite;
}
`;
const GameInfoPanel = ({ appId }) => {
    const { data: game } = useGameInfo(appId);
    const { data: meta } = useGameMetadata(appId);
    const { mode } = useViewMode();
    const [synopsisOpen, setSynopsisOpen] = SP_REACT.useState(false);
    // QAM-driven hide: matches staging's "simple" mode (panel absent).
    if (mode === "compact")
        return null;
    // Wait for both fetches before rendering anything — staging's
    // panel has no transitional / spinner / error UI, and a partial
    // render would flash the wrong layout.
    if (!game || !meta)
        return null;
    return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("style", { children: PANEL_FOCUS_CSS }), SP_JSX.jsxs(DFL.Focusable, { "flow-children": "column", style: {
                    display: "flex",
                    flexDirection: "column",
                    gap: 16,
                    padding: 12,
                }, children: [SP_JSX.jsx(GameInfoCompatRow, { appId: appId, meta: meta, synopsisOpen: synopsisOpen, onToggleSynopsis: () => setSynopsisOpen((v) => !v) }), SP_JSX.jsx(GameInfoInfoRow, { appId: appId, game: game, meta: meta }), game.store === "gog" && (SP_JSX.jsx(GameAchievementsSummary, { store: game.store, gameId: game.id, title: game.title })), synopsisOpen && meta.description && (SP_JSX.jsx(GameInfoSynopsisSection, { description: meta.description })), SP_JSX.jsx(GameInfoNavButtons, { meta: meta })] })] }));
};

/** Apply the patch via the given bridge. Returns a handle
 *  whose `.remove()` undoes both injections. */
function applyAppDetailsPatch(bridge) {
    return bridge.addRouterPatch("/library/app/:appid", (routerTree) => {
        const routeProps = findInReactTree(routerTree, (x) => x?.renderFunc != null);
        if (!routeProps)
            return routerTree;
        // Skip if we've already wrapped this renderFunc. The marker is
        // Unifideck-specific so it doesn't collide with ProtonDB / HLTB.
        if (routeProps.renderFunc.__unifideckPatched) {
            return routerTree;
        }
        const patchHandler = DFL.createReactTreePatcher([
            (tree) => findInReactTree(tree, (x) => x?.props?.children?.props
                ?.overview != null)?.props?.children,
        ], (_args, ret) => {
            try {
                injectIntoTree(ret);
            }
            catch (e) {
                console.error("[Unifideck] AppDetailsPatch handler error:", e);
            }
            return ret;
        });
        DFL.afterPatch(routeProps, "renderFunc", patchHandler);
        routeProps.renderFunc.__unifideckPatched = true;
        return routerTree;
    });
}
/** Walk the rendered route tree and splice our components
 *  into the `InnerContainer` for non-Steam shortcuts. */
function injectIntoTree(ret) {
    const overviewNode = findInReactTree(ret, (x) => x?.props?.children?.props?.overview != null);
    const overview = overviewNode?.props?.children?.props?.overview;
    if (!overview)
        return;
    const appId = overview.appid;
    // Only override non-Steam shortcuts (appId > 2 billion).
    if (!(appId > 2000000000))
        return;
    // Only override Unifideck-managed games — never the user's own
    // non-Steam shortcuts (Firefox, etc.). Skip once we KNOW it isn't
    // ours; stay optimistic before the cache loads so a Unifideck game
    // opened on cold boot never flashes the native UI. This single gate
    // covers the hide marker, the Play wrapper, and the GameInfo panel.
    if (isUnifideckCacheLoaded() && !isUnifideckGame(appId))
        return;
    // Trigger Steam-Store metadata spoofing for this shortcut so
    // Steam's own UI (capsule image, tile, presence) renders the
    // matched Steam game. Fire-and-forget — the patcher reads from
    // its in-memory cache; the backend RPC is a no-op stub.
    void injectGameToAppinfo(appId);
    const innerContainer = findInReactTree(ret, (x) => {
        const n = x;
        return (Array.isArray(n?.props?.children) &&
            typeof n?.props?.className === "string" &&
            DFL.appDetailsClasses?.InnerContainer != null &&
            n.props.className.includes(DFL.appDetailsClasses.InnerContainer));
    });
    // If the container isn't ready yet, skip — the patcher fires
    // again on the next render.
    if (!innerContainer ||
        !innerContainer.props ||
        !Array.isArray(innerContainer.props.children))
        return;
    // Mark the inner container so our scoped CSS rule (nativePlayHideCss)
    // hides Steam's native Play row — which renders in a *separate* subtree
    // beneath this same container, so it can't be removed in-tree. Set
    // synchronously here (re-applied every render; idempotent) instead of
    // the old async CDP `display:none`. Only shortcuts reach this code, so
    // native Steam games are never marked.
    const containerClassName = typeof innerContainer.props.className === "string"
        ? innerContainer.props.className
        : "";
    if (!containerClassName.includes(HIDE_NATIVE_PLAY_MARKER)) {
        innerContainer.props.className =
            `${containerClassName} ${HIDE_NATIVE_PLAY_MARKER}`.trim();
    }
    const children = innerContainer.props.children;
    const playWrapperKey = `unifideck-play-wrapper-${appId}`;
    const gameInfoKey = `unifideck-game-info-${appId}`;
    const version = getGameStateVersion(appId);
    injectPlayWrapper(children, appId, playWrapperKey, version);
    injectGameInfoPanel(children, appId, playWrapperKey, gameInfoKey, version);
    relocateHltbAbovePlay(children, playWrapperKey);
}
/** Relocate HLTB for Deck's stats overlay (`#hltb-for-deck`) to the
 *  hero/Play seam — the same spot it occupies in the *native* Steam UI
 *  (right after the header, before the content panel).
 *
 *  On a native game HLTB splices its element just before Steam's content
 *  panel, so it lands at the hero bottom and its own CSS (transparent bar
 *  for `default`/`clean-default`, hero-anchored box for `clean`/
 *  `clean-left`) renders correctly. In our layout the same splice point
 *  lands LOW (after our injected Play + GameInfo, before the hidden tabbed
 *  section), so the overlay drifts down onto our custom buttons — that
 *  drift, not HLTB's styling, is the bug. Moving its wrapper back to the
 *  seam makes the `default` / `clean-default` BAR modes render pixel-
 *  identical to native with ZERO style changes. The `clean` / `clean-left`
 *  BOX modes get one tiny position-only nudge ({@link HLTB_CLEAN_BOX_POSITION_CSS})
 *  because HLTB's own `top:-55vh` overshoots the box off-screen from the
 *  seam; everything else (transparency, size, side) stays HLTB's own.
 *
 *  Safe because our route patch is the OUTERMOST renderFunc wrapper (we
 *  register after HLTB), so its element is already in `children` when this
 *  runs. No-op — and therefore graceful — when HLTB isn't installed /
 *  co-patching. */
function relocateHltbAbovePlay(children, playWrapperKey) {
    const hltbIdx = children.findIndex((c) => idOf(c) === "hltb-for-deck");
    if (hltbIdx === -1)
        return; // HLTB not present this render
    const playIdx = children.findIndex((c) => keyOf(c).startsWith(playWrapperKey));
    if (playIdx === -1)
        return; // our Play wrapper not placed this render
    if (hltbIdx === playIdx - 1)
        return; // already directly above — idempotent
    const [hltbEl] = children.splice(hltbIdx, 1);
    // Recompute the Play wrapper index — the splice above shifts it when
    // HLTB sat before it.
    const newPlayIdx = children.findIndex((c) => keyOf(c).startsWith(playWrapperKey));
    if (newPlayIdx === -1) {
        children.splice(hltbIdx, 0, hltbEl); // lost the anchor — don't drop HLTB
        return;
    }
    children.splice(newPlayIdx, 0, hltbEl);
}
function injectPlayWrapper(children, appId, baseKey, version) {
    const existingIdx = children.findIndex((c) => keyOf(c).startsWith(baseKey));
    if (existingIdx === -1) {
        const idx = findPlaySectionInsertIndex(children);
        if (idx < 0) {
            // Anchor not found yet — skip injection this render. The
            // patcher re-fires on the next Steam render; the action-bar
            // container usually arrives within one tick. We deliberately
            // don't fall back to a synthetic "after first child" index
            // because that lands the wrapper in the top strip (next to
            // UNSUPPORTED / Details / Synopsis).
            return;
        }
        // Wrap in DownloadProvider — the only context the
        // injected components need (useDownloads in
        // GameInfoCompatRow and usePlaySection). Steam's React
        // tree is rendered outside our RootProvider.
        children.splice(idx, 0, SP_JSX.jsx(DownloadProvider, { children: SP_JSX.jsx(PlaySectionWrapper, { appId: appId }) }, `${baseKey}-v${version}`));
        return;
    }
    // Position-correction : reposition if drifted past index 3.
    if (existingIdx > 3) {
        const [el] = children.splice(existingIdx, 1);
        const correctIdx = findPlaySectionInsertIndex(children);
        if (correctIdx < 0) {
            // Re-insert at the original index — we lost the anchor
            // mid-frame ; don't lose the wrapper entirely.
            children.splice(existingIdx, 0, el);
            return;
        }
        children.splice(correctIdx, 0, el);
    }
}
function injectGameInfoPanel(children, appId, wrapperKey, baseKey, version) {
    const existingIdx = children.findIndex((c) => keyOf(c).startsWith(baseKey));
    if (existingIdx === -1) {
        const wrapperIdx = children.findIndex((c) => keyOf(c).startsWith(wrapperKey));
        let idx;
        if (wrapperIdx >= 0) {
            idx = wrapperIdx + 1;
        }
        else {
            const anchor = findPlaySectionInsertIndex(children);
            if (anchor < 0)
                return; // wait for next render — see injectPlayWrapper
            idx = anchor + 1;
        }
        // DownloadProvider needed by GameInfoCompatRow's useDownloads.
        children.splice(idx, 0, SP_JSX.jsx(DownloadProvider, { children: SP_JSX.jsx(GameInfoPanel, { appId: appId }) }, `${baseKey}-v${version}`));
        return;
    }
    // Position-correction : keep panel immediately after wrapper.
    const wrapperIdx = children.findIndex((c) => keyOf(c).startsWith(wrapperKey));
    if (wrapperIdx >= 0 && existingIdx !== wrapperIdx + 1) {
        const [el] = children.splice(existingIdx, 1);
        const newWrapperIdx = children.findIndex((c) => keyOf(c).startsWith(wrapperKey));
        children.splice(newWrapperIdx + 1, 0, el);
    }
}
/** Resolve where PlaySectionWrapper should land in the
 *  `InnerContainer`'s children array. Tries (in order) :
 *    1. Right after the native PlaySection container.
 *    2. Right after the AppDetails header / TopCapsule.
 *  Returns `-1` if neither anchor is present — caller skips
 *  injection this render and retries on the next one. We
 *  *intentionally* do NOT fall back to a synthetic position
 *  (e.g. "after first non-Unifideck child") because Steam's
 *  InnerContainer also holds the top action strip
 *  (UNSUPPORTED / Details / Synopsis) and the wrong fallback
 *  plants the wrapper there.
 */
function findPlaySectionInsertIndex(children) {
    if (DFL.playSectionClasses?.Container) {
        const idx = children.findIndex((c) => classOf(c).includes(DFL.playSectionClasses.Container));
        if (idx >= 0)
            return idx + 1;
    }
    const headerIdx = children.findIndex((c) => {
        const cn = classOf(c);
        return ((DFL.appDetailsHeaderClasses?.TopCapsule &&
            cn.includes(DFL.appDetailsHeaderClasses.TopCapsule)) ||
            (DFL.appDetailsClasses?.Header && cn.includes(DFL.appDetailsClasses.Header)));
    });
    if (headerIdx >= 0)
        return headerIdx + 1;
    return -1;
}
function keyOf(node) {
    return node?.key ?? "";
}
/** Read a React element's `props.id` (used to find HLTB's
 *  `id="hltb-for-deck"` element spliced into our container). */
function idOf(node) {
    const id = node?.props?.id;
    return typeof id === "string" ? id : "";
}
function classOf(node) {
    const cn = node?.props?.className;
    return typeof cn === "string" ? cn : "";
}

/**
 * Library tab injection — patches Steam's `/library` route to splice
 * the custom Unifideck tabs into the tabbed page React tree.
 *
 * Ported from staging:src/tabs/LibraryPatch.ts. The technique:
 *
 *  1. Patch the `/library` route via `routerHook.addPatch`.
 *  2. `afterPatch` the outer + inner library elements until we reach
 *     the memoised component that owns the tabs array.
 *  3. Intercept its `useMemo` call to splice Unifideck tabs into the
 *     returned tab list before React commits.
 *
 * When TabMaster is installed we skip injection and rely on the
 * `[Unifideck]` Steam Collections (managed by CollectionManager).
 */

let cachedTabAppGrid;
function getReactHooks() {
    const reactInternals = window.SP_REACT;
    const current = reactInternals?.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
        ?.ReactCurrentDispatcher?.current;
    if (current?.useMemo)
        return current;
    const clientInternals = reactInternals?.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
    if (clientInternals) {
        for (const candidate of Object.values(clientInternals)) {
            if (candidate?.useMemo && candidate.useEffect)
                return candidate;
        }
    }
    return null;
}
function detectTabsShape(result) {
    if (result.length >= 2 &&
        Array.isArray(result[0]) &&
        result[0][0]?.id &&
        result[0][0]?.content) {
        return {
            isTuple: true,
            tabs: result[0],
            rest: result.slice(1),
        };
    }
    const first = result[0];
    if (first?.id && first.content) {
        return { isTuple: false, tabs: result, rest: [] };
    }
    return null;
}
// One-shot diagnostic logger — when something bails, log once
// per reason so the console isn't spammed every render.
const loggedBailReasons = new Set();
function logBailOnce(reason, extra) {
    if (loggedBailReasons.has(reason))
        return;
    loggedBailReasons.add(reason);
    console.warn(`[Unifideck Library] spliceTabs bailed: ${reason}`, extra ?? "");
}
function isAppFilter(v) {
    return (typeof v === "object" &&
        v !== null &&
        typeof v.Matches === "function");
}
/** Locate Steam's collections app filter in the tabs useMemo deps.
 *  Matched by shape, never by position: Steam reorders this deps
 *  array across client builds (the 2026-07 beta inserted a dep,
 *  shifting the filter from index 6 to 7 and putting a collection —
 *  no ``Matches`` — where we used to read the filter). The filter is
 *  the only dep with a callable ``Matches``. */
function resolveAppFilter(deps) {
    const found = deps.find(isAppFilter);
    if (!found && !loggedBailReasons.has("app-filter-missing")) {
        loggedBailReasons.add("app-filter-missing");
        console.warn("[Unifideck Library] collections app filter not found in useMemo deps;" +
            " tab counts fall back to unfiltered", deps.map((d) => typeof d));
    }
    return found;
}
function spliceTabs(result, deps) {
    if (!Array.isArray(result)) {
        logBailOnce("result is not an array", result);
        return result;
    }
    const shape = detectTabsShape(result);
    if (!shape) {
        logBailOnce("detectTabsShape returned null", result);
        return result;
    }
    // Idempotency — re-renders of the same route would otherwise
    // splice multiple times and duplicate our tabs.
    if (shape.tabs.some((t) => t.id?.startsWith("unifideck-")))
        return result;
    if (!tabManager.isInitialized()) {
        logBailOnce("tabManager not initialized");
        return result;
    }
    const [eSortBy, setSortBy, showSortingContextMenu] = deps;
    const sortingProps = { eSortBy, setSortBy, showSortingContextMenu };
    const collectionsAppFilterGamepad = resolveAppFilter(deps);
    const template = shape.tabs.find((t) => t.id === "AllGames");
    if (!template) {
        logBailOnce("no AllGames template tab in", shape.tabs.map((t) => t.id));
        return result;
    }
    const TabAppGrid = cachedTabAppGrid ??
        DFL.findInReactTree(template.content, (elt) => Boolean(elt?.type?.toString?.().includes("Library_FilteredByHeader")))?.type;
    if (!TabAppGrid) {
        logBailOnce("Library_FilteredByHeader component not found in AllGames template");
        return result;
    }
    cachedTabAppGrid = TabAppGrid;
    const TabContext = template.content.type?._context ?? null;
    const templateFooter = (template.footer ?? {});
    const customTabs = tabManager
        .getTabs()
        .map((c) => c.getActualTab(TabAppGrid, TabContext, sortingProps, collectionsAppFilterGamepad, templateFooter))
        .filter((t) => t !== null);
    console.log(`[Unifideck Library] splicing ${customTabs.length} custom tabs into Steam library`);
    const hidden = getHiddenDefaultTabs();
    const filteredDefaults = shape.tabs.filter((t) => !hidden.includes(t.id));
    const merged = [...customTabs, ...filteredDefaults];
    if (shape.isTuple)
        return [merged, ...shape.rest];
    return merged;
}
/** Patches the `/library` route to inject Unifideck tabs.
 *  Same pattern as TabMaster — proven to work in both Desktop
 *  and Gaming Mode. */
function applyLibraryPatch(bridge) {
    tabManager.initialize();
    return bridge.addRouterPatch("/library", (rawProps) => {
        const props = rawProps;
        if (isTabMasterInstalled())
            return props;
        const [, setVersion] = SP_REACT.useState(0);
        SP_REACT.useEffect(() => {
            return tabManager.onTabsChanged(() => {
                setVersion((v) => v + 1);
            });
        }, []);
        DFL.afterPatch(props.children, "type", (_, ret1) => {
            if (!ret1?.type)
                return ret1;
            let innerPatch;
            let memoCache;
            SP_REACT.useEffect(() => () => {
                innerPatch?.unpatch();
            });
            DFL.afterPatch(ret1, "type", (_2, ret2) => {
                if (!ret2?.type)
                    return ret2;
                const ret2t = ret2;
                if (memoCache) {
                    ret2t.type = memoCache;
                    return ret2;
                }
                const origMemoComponent = ret2t.type.type;
                // Newer Steam builds can leave `.type.type` null/undefined on
                // some render paths; wrapping it would make the replaced fn call
                // `undefined(...args)` and crash the whole library tab. Bail to
                // the untouched tree if there's no inner component to wrap.
                if (typeof origMemoComponent !== "function")
                    return ret2;
                DFL.wrapReactType(ret2);
                innerPatch = DFL.replacePatch(ret2t.type, "type", (args) => {
                    const hooks = getReactHooks();
                    if (!hooks?.useMemo)
                        return origMemoComponent(...args);
                    const realUseMemo = hooks.useMemo;
                    hooks.useMemo = (fn, deps) => {
                        const enrichedDeps = [...deps, tabManager.getVersion()];
                        return realUseMemo(() => spliceTabs(fn(), enrichedDeps), enrichedDeps);
                    };
                    try {
                        return origMemoComponent(...args);
                    }
                    finally {
                        hooks.useMemo = realUseMemo;
                    }
                });
                memoCache = ret2t.type;
                return ret2;
            });
            return ret1;
        });
        return props;
    });
}

/**
 * Overview enrichment — make Steam's NATIVE library Sort menu and
 * Library Filters work for non-Steam (Unifideck) shortcuts.
 *
 * Our custom tabs render through Steam's own grid, which sorts and
 * filters purely on `AppOverview` field values. Spoofed shortcuts have
 * those empty, so this module writes them from:
 *  - per-shortcut `FacetRecord`s (`library-facets`) — metacritic,
 *    deck-compat, store categories/tags, release date, reviews.
 *    NOT date-added: see `applyFacet` — writing it into
 *    `rt_purchased_time` hijacked Steam's Home "Recent Games" shelf;
 *  - Steam's own playtime store (`SteamClient.Apps.GetPlaytime`); and
 *  - the backend `get_game_size_bytes` RPC (on-disk size, installed).
 *
 * PERSISTENCE: Steam re-creates/re-sets a shortcut's `AppOverview`
 * (e.g. after closing its details page), which WIPES any fields we
 * wrote — the game then drops out of Great-on-Deck and loses its
 * metacritic. So, like SDH-PlayTime, we patch `appStore.m_mapApps.set`
 * to re-apply our enrichment on every overview write, and cache the
 * async-sourced values (playtime, size) so the patch can re-apply them
 * synchronously.
 *
 * Write mechanisms (verified live via CEF — plan Phase-0):
 *  - plain writable fields → assign (metacritic, rt_* dates, reviews,
 *    playtime, size);
 *  - `store_category` getter → mutate `m_setStoreCategories` (Players);
 *  - `steam_deck_compat_category` getter → write
 *    `steam_hw_compat_category_packed` (Deck-compat).
 */
const NON_STEAM_APP_TYPE = 1073741824;
// Async-sourced values cached so the m_mapApps.set patch can re-apply
// them synchronously when Steam re-sets an overview.
const playtimeByAppId = new Map();
const sizeByAppId = new Map();
const sizeFetched = new Set();
function getAppStore() {
    return window.appStore ?? null;
}
function getAppsApi() {
    return (window.SteamClient
        ?.Apps ?? null);
}
function forEachShortcutOverview(cb) {
    const map = getAppStore()?.m_mapApps;
    if (!map)
        return;
    for (const ov of map.values()) {
        if (ov && ov.app_type === NON_STEAM_APP_TYPE)
            cb(ov);
    }
}
/** "21 Jun, 2022" → unix seconds, or 0 when unparseable. */
function releaseUnix(dateStr) {
    if (!dateStr)
        return 0;
    const ms = Date.parse(dateStr);
    return Number.isNaN(ms) ? 0 : Math.floor(ms / 1000);
}
/** Reconcile a MobX-backed id set to exactly `ids` (clear + add). Safe
 *  because we only call this on non-Steam shortcuts, whose sets Steam
 *  itself never populates. */
function setIds(target, ids) {
    if (!target || typeof target.add !== "function")
        return;
    try {
        target.clear();
        for (const id of ids)
            target.add(id);
    }
    catch {
        /* not a real Set on this Steam build — skip */
    }
}
function applyFacet(ov, facet) {
    if (typeof facet.metacritic === "number")
        ov.metacritic_score = facet.metacritic;
    const rel = releaseUnix(facet.release_date);
    if (rel > 0) {
        ov.rt_original_release_date = rel;
        ov.rt_steam_release_date = rel;
    }
    // Steam's Home "Recent Games" shelf ranks its candidates by
    //   max(rt_last_time_locally_played, rt_purchased_time,
    //       installed ? rt_last_time_played_or_installed : 0)
    // over a pool that ALREADY includes every non-Steam shortcut (the
    // LocalGames collection). Stock Steam leaves shortcuts at 0 so they
    // sort harmlessly to the bottom. Projecting our `date_added_unix`
    // into `rt_purchased_time` (to power the native "Date Added" sort)
    // gave every freshly-synced game a recency of *now* and floated the
    // whole library to the top of the shelf. Keep the field at 0.
    // Scrubbing — rather than simply not writing — also clears values a
    // previous build left on live overviews when the plugin is reloaded
    // without a Steam restart.
    if (ov.rt_purchased_time)
        ov.rt_purchased_time = 0;
    if (typeof facet.review_score === "number") {
        ov.review_score_with_bombs = facet.review_score;
    }
    if (typeof facet.review_percentage === "number") {
        ov.review_percentage_with_bombs = facet.review_percentage;
    }
    if (facet.deck_category > 0) {
        const cur = ov.steam_hw_compat_category_packed ?? 0;
        ov.steam_hw_compat_category_packed = (cur & -4) | (facet.deck_category & 3);
    }
    setIds(ov.m_setStoreCategories, facet.store_category);
    setIds(ov.m_setStoreTags, facet.store_tag);
}
/** Apply EVERYTHING we know for one shortcut overview (facet + cached
 *  playtime + cached size). Cheap and idempotent; used by the bulk
 *  passes AND the m_mapApps.set patch. */
function applyEnrichment(ov) {
    if (!ov || ov.app_type !== NON_STEAM_APP_TYPE)
        return;
    const facet = getFacet(ov.appid);
    if (facet)
        applyFacet(ov, facet);
    const pt = playtimeByAppId.get(ov.appid);
    if (pt) {
        if (pt.mins > 0)
            ov.minutes_playtime_forever = pt.mins;
        if (pt.last > 0)
            ov.rt_last_time_played = pt.last;
    }
    const sz = sizeByAppId.get(ov.appid);
    if (typeof sz === "number" && sz > 0)
        ov.size_on_disk = sz;
}
/** Re-apply enrichment to every shortcut overview currently in the map. */
function enrichAllShortcuts() {
    let count = 0;
    forEachShortcutOverview((ov) => {
        if (getFacet(ov.appid)) {
            applyEnrichment(ov);
            count++;
        }
    });
    if (count > 0) {
        console.log(`[Unifideck] Enriched ${count} shortcut overviews`);
    }
}
/**
 * Pull each shortcut's playtime from Steam's own store (`GetPlaytime`)
 * — the overview's `minutes_playtime_forever` is 0 for shortcuts, but
 * Steam tracks the real value separately. Cache it (for the set-patch)
 * and write it onto the live overview.
 */
async function enrichPlaytime() {
    const apps = getAppsApi();
    if (!apps?.GetPlaytime)
        return;
    const overviews = [];
    forEachShortcutOverview((ov) => overviews.push(ov));
    let count = 0;
    const CHUNK = 25;
    for (let i = 0; i < overviews.length; i += CHUNK) {
        await Promise.all(overviews.slice(i, i + CHUNK).map(async (ov) => {
            try {
                const r = await apps.GetPlaytime(ov.appid);
                if (!r)
                    return;
                const mins = typeof r.nPlaytimeForever === "number" ? r.nPlaytimeForever : 0;
                const last = typeof r.rtLastTimePlayed === "number" ? r.rtLastTimePlayed : 0;
                if (mins > 0 || last > 0) {
                    playtimeByAppId.set(ov.appid, { mins, last });
                    if (mins > 0)
                        ov.minutes_playtime_forever = mins;
                    if (last > 0)
                        ov.rt_last_time_played = last;
                    count++;
                }
            }
            catch {
                /* skip this app */
            }
        }));
    }
    if (count > 0) {
        console.log(`[Unifideck] Enriched playtime for ${count} shortcuts`);
    }
}
/**
 * Populate `size_on_disk` for INSTALLED shortcuts via the backend
 * `get_game_size_bytes` RPC. The RPC can shell out, so this is a
 * polite one-shot background trickle (each game fetched at most once
 * per session; results cached for the set-patch).
 */
async function enrichSizes() {
    const targets = [];
    forEachShortcutOverview((ov) => {
        if (sizeFetched.has(ov.appid))
            return;
        if (!unifideckGameCache.get(ov.appid)?.isInstalled)
            return;
        targets.push(ov);
    });
    for (const ov of targets) {
        sizeFetched.add(ov.appid);
        try {
            const raw = await call(rpcRoutes.getGameSizeBytes, ov.appid);
            const bytes = unwrapRpcEnvelope(raw, {
                route: rpcRoutes.getGameSizeBytes,
            });
            if (typeof bytes === "number" && bytes > 0) {
                sizeByAppId.set(ov.appid, bytes);
                ov.size_on_disk = bytes;
            }
        }
        catch {
            /* leave size unset for this game */
        }
        await new Promise((r) => setTimeout(r, 120));
    }
}
/**
 * Patch `appStore.m_mapApps.set` so our enrichment is re-applied every
 * time Steam writes a shortcut's overview — otherwise closing a game's
 * details page (which re-sets the overview) wipes our fields and the
 * game drops out of Great-on-Deck / loses its metacritic. Mirrors
 * SDH-PlayTime's `steamPlayTimePatches`. Returns a disposer.
 */
function patchAppMapSet() {
    const map = getAppStore()?.m_mapApps;
    if (!map || typeof map.set !== "function" || map.__unifideckOriginalSet) {
        return () => { };
    }
    const originalSet = map.set.bind(map);
    map.__unifideckOriginalSet = originalSet;
    map.set = (appId, overview) => {
        try {
            applyEnrichment(overview);
        }
        catch {
            /* never break Steam's own write path */
        }
        return originalSet(appId, overview);
    };
    return () => {
        if (map.__unifideckOriginalSet) {
            map.set = map.__unifideckOriginalSet;
            delete map.__unifideckOriginalSet;
        }
    };
}
let started$1 = false;
/**
 * Boot entry — load facets, enrich, install the persistence patch, and
 * keep overviews fresh. Invoked from `definePlugin` (boot), NOT the QAM
 * panel, so the native Sort/Filters work in Gaming Mode.
 */
function startOverviewEnrichment() {
    if (started$1)
        return () => { };
    started$1 = true;
    let unpatchMap = () => { };
    const run = () => {
        enrichAllShortcuts();
        void enrichPlaytime();
        void enrichSizes();
    };
    const runWhenReady = (attempt = 0) => {
        const appStore = getAppStore();
        const ready = !!appStore?.m_mapApps && appStore.m_mapApps.size > 0;
        if (!ready && attempt < 15) {
            setTimeout(() => runWhenReady(attempt + 1), 1000);
            return;
        }
        unpatchMap = patchAppMapSet();
        run();
    };
    void loadFacets().then(() => runWhenReady());
    const onSync = () => {
        void loadFacets(true).then(run);
        setTimeout(() => {
            void loadFacets(true).then(run);
        }, 8000);
    };
    const onState = () => run();
    window.addEventListener("unifideck-sync-completed", onSync);
    window.addEventListener("unifideck-game-state-changed", onState);
    const unsubStop = EventBusClient.subscribe(Events.GAME_STOPPED, () => {
        setTimeout(() => {
            void enrichPlaytime();
        }, 3000);
    });
    // The metacritic backfill finishes in the background AFTER the sync's
    // progress bar hits 100% (and after `onSync`'s short retry window), so
    // its long-tail scores would otherwise only appear on the next
    // resync/restart. Re-read facets + re-enrich when it signals done.
    const unsubBackfill = EventBusClient.subscribe(Events.METADATA_BACKFILL_COMPLETE, () => {
        void loadFacets(true).then(run);
    });
    return () => {
        window.removeEventListener("unifideck-sync-completed", onSync);
        window.removeEventListener("unifideck-game-state-changed", onState);
        try {
            unsubStop?.();
        }
        catch {
            /* ignore */
        }
        try {
            unsubBackfill?.();
        }
        catch {
            /* ignore */
        }
        try {
            unpatchMap();
        }
        catch {
            /* ignore */
        }
    };
}

/**
 * Library grid-tile store badge — render each non-Steam (Unifideck)
 * shortcut's STORE logo (Epic/GOG/Amazon/Xbox/Ubisoft) in place of the
 * Steam Deck 'D' glyph on its library tile, keeping the compatibility
 * status icon ("Proton badge") next to it.
 *
 * Steam hides the Deck-compat badge for shortcuts
 * (`overview.BIsModOrShortcut() ? null : …`), so non-Steam tiles show
 * no corner badge. This patches the library-tile component so our
 * shortcuts get a badge that matches native styling.
 *
 * HOW (verified live via CEF/CDP):
 *  - The tile is the exported `We` memo (module factory source contains
 *    `LibraryItemIcons` + `BIsModOrShortcut` + `BIsMusicAlbum`). It's a
 *    `React.memo` whose `.type` (the mobx-observer render fn) is a
 *    writable own-property — we patch THAT (never the non-configurable
 *    module export). The memo object is shared across SharedJSContext +
 *    the BPM window and renders both the portrait and wide grid.
 *  - The badge component `C.$o` (factory source contains
 *    `SteamDeckCompatInfo`) renders
 *    `<div class={SteamDeckCompatInfo,…}>[ <DeckD/>, <StatusIcon/> ]</div>`.
 *    We reuse its output (so the status icon comes for free) and swap
 *    `children[0]` (the Deck 'D') for a `<StoreIcon>`.
 *  - Bottom-right placement + show-on-highlight are pure CSS keyed on the
 *    `SteamDeckCompatIcon` class (`margin-inline-start:auto`,
 *    `opacity:0` + a `:hover` rule). We pass that class (with
 *    `DeckCompat`) to `C.$o`, exactly as native does, so we inherit both.
 *
 * SAFETY: this only READS `BIsModOrShortcut()` (to pick which tiles to
 * decorate) and ADDS an element to the rendered output. It never writes
 * `appStore`, never proxies the overview — so launch routing (RunGame),
 * which reads the real overview, is unaffected. `StoreBadge` returns
 * `null` on any error so it can never break the grid.
 */

const REACT_MEMO = Symbol.for("react.memo");
// ── resolved-once singletons ──────────────────────────────
let started = false;
let memoObject = null;
let originalType = null;
let deckCompatBadge = null;
/** Resolved obfuscated CSS classes (rotate per Steam build). */
let rowClass = ""; // LibraryItemIcons — the icon row we inject into
let deckClass = ""; // DeckCompat + SteamDeckCompatIcon — drives bottom-right + show-on-highlight
let infoClass = ""; // SteamDeckCompatInfo — the badge container (cat==0 fallback)
const warned = new Set();
function warnOnce(reason, extra) {
    if (warned.has(reason))
        return;
    warned.add(reason);
    console.warn(`[Unifideck TileBadge] ${reason}`, extra ?? "");
}
function getWebpackRequire() {
    const w = window;
    if (!Array.isArray(w.webpackChunksteamui))
        return null;
    let req;
    try {
        w.webpackChunksteamui.push([
            [`unifideck_tilebadge_${Math.random()}`],
            {},
            (r) => {
                req = r;
            },
        ]);
    }
    catch {
        return null;
    }
    return req && req.m ? req : null;
}
/** Find the tile module by its factory source (immune to runtime
 *  patching of instantiated exports) and return its `React.memo`. */
function findTileMemo(req) {
    for (const id of Object.keys(req.m)) {
        let src = "";
        try {
            src = String(req.m[id]);
        }
        catch {
            continue;
        }
        if (!src.includes("LibraryItemIcons") ||
            !src.includes("BIsModOrShortcut") ||
            !src.includes("BIsMusicAlbum")) {
            continue;
        }
        let exports$1;
        try {
            exports$1 = req(id);
        }
        catch {
            continue;
        }
        for (const key of Object.keys(exports$1)) {
            let v;
            try {
                v = exports$1[key];
            }
            catch {
                continue;
            }
            const memo = v;
            if (memo &&
                memo.$$typeof === REACT_MEMO &&
                typeof memo.type === "function") {
                return memo;
            }
        }
    }
    return null;
}
function strProp(o, key) {
    try {
        const v = o[key];
        return typeof v === "string" ? v : null;
    }
    catch {
        return null;
    }
}
/** Resolve `C.$o` + the CSS classes in one pass over the bundle. */
function resolveBadgeRefs(req) {
    let iconCls = null;
    let deckCompatCls = null;
    for (const id of Object.keys(req.m)) {
        let exports$1;
        try {
            exports$1 = req(id);
        }
        catch {
            continue;
        }
        if (!exports$1 || typeof exports$1 !== "object")
            continue;
        for (const o of [exports$1, exports$1.default]) {
            if (!o || typeof o !== "object")
                continue;
            if (!rowClass) {
                const lii = strProp(o, "LibraryItemIcons");
                const sdci = strProp(o, "SteamDeckCompatIcon");
                if (lii && sdci) {
                    rowClass = lii;
                    iconCls = sdci;
                }
            }
            if (!deckCompatCls)
                deckCompatCls = strProp(o, "DeckCompat");
            if (!infoClass)
                infoClass = strProp(o, "SteamDeckCompatInfo") ?? "";
        }
        if (!deckCompatBadge) {
            let keys = [];
            try {
                keys = Object.keys(exports$1);
            }
            catch {
                keys = [];
            }
            for (const k of keys) {
                let v;
                try {
                    v = exports$1[k];
                }
                catch {
                    continue;
                }
                if (typeof v !== "function")
                    continue;
                let src = "";
                try {
                    src = v.toString();
                }
                catch {
                    continue;
                }
                if (src.includes("SteamDeckCompatInfo") && src.length < 500) {
                    deckCompatBadge = v;
                    break;
                }
            }
        }
        if (rowClass && iconCls && deckCompatCls && infoClass && deckCompatBadge) {
            break;
        }
    }
    if (!rowClass || !iconCls)
        return false;
    // DeckCompat + SteamDeckCompatIcon — the className native passes; the
    // SteamDeckCompatIcon part is what the CSS rule keys on for placement.
    deckClass = (deckCompatCls ? `${deckCompatCls} ` : "") + iconCls;
    return !!deckCompatBadge;
}
/**
 * The injected badge. For shortcuts WITH compat data it reuses Steam's
 * own `C.$o` output (correct container + status icon) and swaps the
 * Deck-'D' (`children[0]`) for the store logo; for shortcuts without
 * compat it renders the store logo alone in a matching container. Any
 * failure returns `null` (no badge) so the tile/grid can't break.
 */
const StoreBadge = ({ category, store, }) => {
    try {
        const logo = SP_REACT.createElement(StoreIcon, { store, size: 16, color: "#fff" });
        if (category > 0 && deckCompatBadge) {
            const base = deckCompatBadge({ category, className: deckClass });
            if (base && base.props) {
                const kids = base.props.children;
                const status = Array.isArray(kids) ? kids[1] : null;
                return SP_REACT.cloneElement(base, undefined, logo, status);
            }
        }
        return SP_REACT.createElement("div", { className: `${infoClass} ${deckClass}` }, logo);
    }
    catch {
        return null;
    }
};
const BADGE_KEY = "unifideck-store-badge";
/**
 * Resolve the true original tile render fn at render time. The library
 * `React.memo` is a webpack singleton that survives plugin reload, but our
 * module-level `originalType` resets to `null` on every fresh module
 * instance — so a wrapper installed by a prior instance would read a null
 * `originalType` and crash. Recover from the durable stash on the memo
 * itself first (survives reload), then the render-time `this`, then the
 * module vars. Never return the wrapper itself (would recurse). Returns
 * `null` only if nothing usable is found, in which case the caller renders
 * an empty Fragment rather than crashing the whole grid.
 */
function resolveOriginal(self) {
    const candidates = [
        wrappedTileType.__unifideckMemo?.__unifideckOrigType,
        self?.__unifideckOrigType,
        originalType ?? undefined,
        memoObject?.__unifideckOrigType,
    ];
    for (const c of candidates) {
        if (typeof c === "function" && c !== wrappedTileType) {
            return c;
        }
    }
    return null;
}
function wrappedTileType(...args) {
    const orig = resolveOriginal(this);
    if (!orig) {
        // Transient reload artifact: the wrapper is still installed on the
        // shared memo but the original can't be recovered. Render nothing for
        // this one tile — an empty Fragment is a valid element that can't trip
        // the mobx-observer render machinery — instead of throwing to Steam's
        // error boundary and taking down the entire library grid. Self-heals
        // on the next full render once install rebinds the original.
        warnOnce("original type unavailable; rendering empty tile");
        return SP_REACT.createElement(SP_REACT.Fragment);
    }
    const props = args[0];
    const app = props?.app;
    // Fast bail for native tiles — only READ BIsModOrShortcut, never write.
    if (!app ||
        typeof app.BIsModOrShortcut !== "function" ||
        !app.BIsModOrShortcut()) {
        return orig.apply(this, args);
    }
    const store = getStoreForApp(app.appid, app.app_type);
    if (!store || store === "steam")
        return orig.apply(this, args);
    let ret;
    try {
        ret = orig.apply(this, args);
    }
    catch {
        return orig.apply(this, args);
    }
    try {
        const row = findInReactTree(ret, (n) => {
            const cls = n?.props?.className;
            return typeof cls === "string" && cls.includes(rowClass);
        });
        if (row?.props) {
            const category = app.steam_deck_compat_category ??
                getFacet(app.appid)?.deck_category ??
                0;
            const badge = SP_REACT.createElement(StoreBadge, {
                key: BADGE_KEY,
                category,
                store: store,
            });
            const kids = row.props.children;
            if (Array.isArray(kids)) {
                if (!kids.some((c) => c?.key === BADGE_KEY)) {
                    kids.push(badge);
                }
            }
            else if (kids != null) {
                row.props.children = [kids, badge];
            }
            else {
                row.props.children = [badge];
            }
        }
    }
    catch (e) {
        warnOnce("inject failed", e);
    }
    return ret;
}
/**
 * Install the patch at boot (before the grid first mounts — `React.memo`
 * caches the resolved inner fn on already-mounted fibers, so boot-time
 * install avoids forced remounts). Returns a disposer. Degrades to a
 * no-op (grid untouched) if the tile component or classes can't be
 * resolved on this Steam build.
 */
function startTileStoreBadgePatch() {
    if (started)
        return stopTileStoreBadgePatch;
    started = true;
    try {
        const req = getWebpackRequire();
        if (!req) {
            warnOnce("webpack require unavailable; tile badges disabled");
            started = false;
            return () => { };
        }
        const memo = findTileMemo(req);
        if (!memo) {
            warnOnce("library-tile memo not found; tile badges disabled");
            started = false;
            return () => { };
        }
        if (!resolveBadgeRefs(req)) {
            warnOnce("deck-compat badge / classes not resolved; tile badges disabled");
            started = false;
            return () => { };
        }
        memoObject = memo;
        // Recover the true original. `memo` is a webpack singleton that
        // survives plugin reload, so on re-inject a prior module instance's
        // wrapper may already sit in `memo.type` with the durable stash on
        // `__unifideckOrigType`. Prefer the stash; never treat our own wrapper
        // as the original (would recurse). If `memo.type` is a stale wrapper
        // with no stash to recover from, we can't safely unwrap it — leave it
        // and bail rather than double-wrapping or stashing the wrapper.
        const currentType = memo.type;
        if (typeof memo.__unifideckOrigType === "function") {
            originalType = memo.__unifideckOrigType;
        }
        else if (!currentType.__unifideckWrapper) {
            originalType = memo.type;
        }
        else {
            warnOnce("stale wrapper without recoverable original; leaving as-is");
            // Rebind this module's back-reference so the live (stale) wrapper's
            // resolver can still find whatever original a later install stashes.
            wrappedTileType.__unifideckMemo = memo;
            started = false;
            return () => { };
        }
        memo.__unifideckOrigType = originalType;
        const wrapper = wrappedTileType;
        wrapper.__unifideckWrapper = true;
        wrapper.__unifideckMemo = memo;
        memo.type = wrappedTileType;
        console.log("[Unifideck] Tile store-badge patch active");
    }
    catch (e) {
        console.error("[Unifideck] tile store-badge patch install failed:", e);
        started = false;
        return () => { };
    }
    return stopTileStoreBadgePatch;
}
function stopTileStoreBadgePatch() {
    if (!started)
        return;
    const memo = memoObject;
    const orig = originalType;
    // Reset the module singletons first — teardown must be idempotent and a
    // second call must not re-enter this body.
    memoObject = null;
    originalType = null;
    started = false;
    if (!memo || !orig)
        return;
    try {
        // Only restore OUR wrapper (never clobber a foreign patch that may have
        // been layered on since install).
        if (memo.type === wrappedTileType) {
            memo.type = orig;
        }
        // Delete the recovery stash ONLY once the live slot is confirmed
        // unwrapped. If the restore above threw or silently didn't take (e.g.
        // `.type` became non-writable on a newer Steam build), leave
        // `__unifideckOrigType` in place so the still-live wrapper's resolver
        // can recover the original next render and a later install can rebind.
        if (memo.type !== wrappedTileType) {
            delete memo.__unifideckOrigType;
        }
    }
    catch (e) {
        console.warn("[Unifideck TileBadge] unpatch failed:", e);
    }
}

const rowStyle = {
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 8,
    width: "100%",
};
const ChangeExecutableModal = ({ store, gameId, gameTitle, closeModal, }) => {
    const { t } = useTranslation();
    const toast = useToast();
    const list = useRPCQuery(rpcRoutes.listGameExecutables, [store, gameId]);
    const setExe = useRPCMutation(rpcRoutes.setGameExecutable);
    const resetExe = useRPCMutation(rpcRoutes.resetGameExecutable);
    const [busy, setBusy] = SP_REACT.useState(false);
    const data = list.data;
    const installDir = data?.install_dir ?? "";
    const working = busy || setExe.loading || resetExe.loading;
    const applySet = async (rel) => {
        setBusy(true);
        try {
            const res = await setExe.mutate(store, gameId, rel);
            if (res && res.success !== false) {
                toast.success(t("play.exe.changed"), res.executable ?? rel);
                await list.refetch();
            }
            else {
                toast.error(t("play.exe.changeFailed"), rel);
            }
        }
        catch {
            toast.error(t("play.exe.changeFailed"), rel);
        }
        finally {
            setBusy(false);
        }
    };
    const applyReset = async () => {
        setBusy(true);
        try {
            const res = await resetExe.mutate(store, gameId);
            if (res && res.success !== false) {
                toast.success(t("play.exe.reset"), gameTitle);
                await list.refetch();
            }
            else {
                toast.error(t("play.exe.changeFailed"), gameTitle);
            }
        }
        finally {
            setBusy(false);
        }
    };
    const browse = async () => {
        if (!installDir)
            return;
        try {
            // NOTE: do NOT pass a RegExp `filter` — Decky forwards it to the Python
            // backend (`utilities/filepicker_ls`), and a RegExp can't cross the
            // JS→Python RPC bridge (it serialises to `{}`), which makes the backend
            // return an empty listing. The `extensions` array IS serialisable and
            // drives both the ".exe / All Files" dropdown and the server-side filter.
            // `includeFolders: true` lets the user navigate into subdirectories.
            const res = await openFilePicker(0 /* FileSelectionType.FILE */, installDir, true, // includeFiles
            true, // includeFolders (navigate; was false → no folders shown)
            undefined, // filter — see note above
            ["exe"], // extensions: dropdown + server-side filter
            false, // showHiddenFiles
            true);
            const abs = res?.realpath || res?.path;
            if (!abs)
                return;
            const base = installDir.endsWith("/") ? installDir : `${installDir}/`;
            const rel = abs.startsWith(base) ? abs.slice(base.length) : abs;
            await applySet(rel);
        }
        catch {
            // user cancelled the picker — no-op
        }
    };
    return (SP_JSX.jsxs(DFL.ConfirmModal, { strTitle: t("play.exe.title", { game: gameTitle }), bAlertDialog: true, strOKButtonText: t("common.close"), onOK: closeModal, onCancel: closeModal, children: [SP_JSX.jsx("div", { style: { marginBottom: 8, opacity: 0.8, fontSize: "0.9em" }, children: t("play.exe.subtitle") }), list.loading && SP_JSX.jsx("div", { children: t("common.loading") }), list.error && SP_JSX.jsx("div", { children: t("play.exe.unavailable") }), SP_JSX.jsx(DFL.Focusable, { style: { display: "flex", flexDirection: "column", gap: 4 }, children: data?.candidates.map((c) => (SP_JSX.jsx(DFL.DialogButton, { disabled: working, onClick: () => void applySet(c.rel), style: { padding: "8px 12px" }, children: SP_JSX.jsxs("div", { style: rowStyle, children: [SP_JSX.jsxs("span", { style: {
                                    overflow: "hidden",
                                    textOverflow: "ellipsis",
                                    whiteSpace: "nowrap",
                                }, children: [c.name, c.is_default ? ` ${t("play.exe.defaultTag")}` : "", SP_JSX.jsx("span", { style: {
                                            opacity: 0.5,
                                            marginInlineStart: 8,
                                            fontSize: "0.8em",
                                        }, children: c.rel })] }), c.is_current && SP_JSX.jsx(FaCheck, {})] }) }, c.rel))) }), SP_JSX.jsxs(DFL.Focusable, { style: { display: "flex", gap: 8, marginTop: 12 }, children: [SP_JSX.jsxs(DFL.DialogButton, { disabled: working || !installDir, onClick: () => void browse(), children: [SP_JSX.jsx(FaFolderOpen, { style: { marginInlineEnd: 8 } }), t("play.exe.browse")] }), data?.override_active && (SP_JSX.jsxs(DFL.DialogButton, { disabled: working, onClick: () => void applyReset(), children: [SP_JSX.jsx(FaUndo, { style: { marginInlineEnd: 8 } }), t("play.exe.resetButton")] }))] })] }));
};

/**
 * app-context-menu-patch — inject "Change executable…" into the native game
 * context menu (the gear / right-click menu with Add to Favorites, Manage,
 * Properties…).
 *
 * Technique ported from decky-steamgriddb's `contextMenuPatch.tsx`: resolve the
 * `LibraryContextMenu` component, `afterPatch` its `render` (+ the inner
 * `type.render` / `shouldComponentUpdate`), and splice a `MenuItem` in just
 * before the "Properties…" entry. Proven robust across Steam client versions.
 *
 * GATING: the item is added only for an INSTALLED Unifideck shortcut whose
 * store supports an executable override (gog / amazon / epic). Regular Steam
 * games — and unsupported stores (Microsoft xCloud) — are left untouched. The
 * patch only ADDS a menu item; it never mutates the overview or launch routing.
 */

/** Stores whose launch target the user can override (see ExecutableRPCMixin). */
const SUPPORTED_STORES = new Set(["gog", "amazon", "epic"]);
/** Stable key so re-renders can dedupe our injected item. */
const MENU_ITEM_KEY = "unifideck-change-exe";
/** A Unifideck shortcut that supports an exe override, or null. */
function eligible(appId) {
    const game = getUnifideckGame(appId);
    if (!game ||
        !game.storeGameId ||
        !game.isInstalled ||
        !SUPPORTED_STORES.has(game.store)) {
        return null;
    }
    return { store: game.store, gameId: game.storeGameId };
}
function openModal(appId) {
    const g = eligible(appId);
    if (!g)
        return;
    const overview = window.appStore?.GetAppOverviewByAppID?.(appId);
    const title = overview?.display_name ?? g.gameId;
    DFL.showModal(SP_REACT.createElement(ChangeExecutableModal, {
        store: g.store,
        gameId: g.gameId,
        gameTitle: String(title),
        closeModal: () => { },
    }));
}
/** Insert our item before "Properties…" (matched by its onSelected source). */
function spliceItem(children, appId) {
    const propsIdx = children.findIndex((item) => DFL.findInReactTree(item, (x) => !!x?.onSelected && x.onSelected.toString().includes("AppProperties")));
    const node = SP_REACT.createElement(DFL.MenuItem, { key: MENU_ITEM_KEY, onSelected: () => openModal(appId) }, instance.t("play.exe.menuItem"));
    if (propsIdx >= 0)
        children.splice(propsIdx, 0, node);
    else
        children.push(node);
}
/** Drop a previously-injected item so a re-render can't duplicate it. */
function dedupe(children) {
    const idx = children.findIndex((x) => x?.key === MENU_ITEM_KEY);
    if (idx !== -1)
        children.splice(idx, 1);
}
/** Re-resolve the appid from the menu's OWN React tree (not a stale closure).
 *
 * The outer-render `appId` is captured once (the first menu ever opened), so
 * trusting it would make every game's menu act on that first game. Mirror
 * decky-steamgriddb: prefer a child whose owner carries a *different* overview
 * appid, then any `app.appid` in the tree, falling back to the closure value.
 */
function resolveItemsAppId(menuItems, fallbackAppId) {
    const items = menuItems;
    const parent = items.find((x) => {
        const a = x?._owner?.pendingProps?.overview?.appid;
        return !!a && a !== fallbackAppId;
    });
    const parentAppId = parent?._owner?.pendingProps?.overview?.appid;
    if (parentAppId)
        return parentAppId;
    const found = DFL.findInTree(menuItems, (x) => !!x?.app?.appid, { walkable: ["props", "children"] });
    return found?.app?.appid ?? fallbackAppId;
}
/** Dedupe, re-resolve the per-menu appid, and (if eligible) splice our item.
 *
 * This is the single choke point for adding the item — every patched render
 * path routes through here so the appid is ALWAYS resolved from the live menu
 * (fixing the "every game shows Fallout NV" stale-closure bug).
 */
function patchMenuItems(menuItems, fallbackAppId) {
    dedupe(menuItems);
    const appId = resolveItemsAppId(menuItems, fallbackAppId);
    if (!eligible(appId))
        return;
    spliceItem(menuItems, appId);
}
/** Heuristic that this is the app context menu (vs a screenshot/other menu). */
function isAppContextMenu(items) {
    if (!Array.isArray(items) || !items.length)
        return false;
    return !!DFL.findInReactTree(items, (x) => !!x?.props?.onSelected &&
        x.props.onSelected.toString().includes("launchSource"));
}
/** Resolve the menu's appid across client versions (overview → app.appid). */
function resolveAppId(component) {
    const fromOwner = component?._owner?.pendingProps?.overview?.appid;
    if (fromOwner)
        return fromOwner;
    const found = DFL.findInTree(component?.props?.children, (x) => !!x?.app?.appid, { walkable: ["props", "children"] });
    return found?.app?.appid ?? 0;
}
/** The `LibraryContextMenu` class component, or null if Steam changed it. */
function resolveLibraryContextMenu() {
    try {
        const mod = DFL.findModuleByExport((e) => !!e?.toString && e.toString().includes("().LibraryContextMenu"));
        const sibling = Object.values(mod ?? {}).find((s) => typeof s?.toString === "function" &&
            s.toString().includes("navigator:"));
        if (!sibling)
            return null;
        return DFL.fakeRenderComponent(sibling).type ?? null;
    }
    catch (e) {
        console.error("[Unifideck] LibraryContextMenu resolve failed:", e);
        return null;
    }
}
/**
 * Patch the native game context menu to add "Change executable…". Returns a
 * handle whose `unpatch()` removes it (called on plugin teardown). A no-op
 * handle is returned if Steam's menu component can't be located.
 */
function applyAppContextMenuPatch() {
    const LibraryContextMenu = resolveLibraryContextMenu();
    if (!LibraryContextMenu) {
        return { unpatch: () => undefined };
    }
    let inner;
    const outer = DFL.afterPatch(LibraryContextMenu.prototype, "render", (_args, component) => {
        // Fallback appid for THIS render; the real one is re-resolved per-menu
        // inside patchMenuItems (the closure value goes stale across opens).
        const appId = resolveAppId(component);
        if (!inner) {
            inner = DFL.afterPatch(component, "type", (_a, ret) => {
                const proto = ret?.type?.prototype;
                if (!proto)
                    return ret;
                DFL.afterPatch(proto, "render", (_b, ret2) => {
                    const menuItems = ret2
                        ?.props?.children?.[0];
                    if (!isAppContextMenu(menuItems))
                        return ret2;
                    try {
                        patchMenuItems(menuItems, appId);
                    }
                    catch (e) {
                        console.error("[Unifideck] context-menu splice failed:", e);
                    }
                    return ret2;
                });
                DFL.afterPatch(proto, "shouldComponentUpdate", (args, shouldUpdate) => {
                    const next = args?.[0]?.children;
                    if (Array.isArray(next)) {
                        try {
                            dedupe(next); // always clear stale, even when not updating
                            if (shouldUpdate === true)
                                patchMenuItems(next, appId);
                        }
                        catch {
                            /* wrong menu — leave it */
                        }
                    }
                    return shouldUpdate;
                });
                return ret;
            });
        }
        else {
            // Subsequent opens: the inner patch is bound to the FIRST menu's
            // component prototype and may not fire for this one — splice directly
            // so the item reliably appears (fixes the "randomly disappears" bug).
            try {
                const children = component
                    ?.props?.children;
                if (Array.isArray(children))
                    patchMenuItems(children, appId);
            }
            catch (e) {
                console.error("[Unifideck] context-menu else-splice failed:", e);
            }
        }
        return component;
    });
    return {
        unpatch: () => {
            try {
                outer?.unpatch();
                inner?.unpatch();
            }
            catch (e) {
                console.error("[Unifideck] context-menu unpatch failed:", e);
            }
        },
    };
}

const AccountSwitchModal = ({ hasRegistry, hasAuthTokens, onMigrate, onClearAuths, onSkip, closeModal, }) => {
    const { t } = useTranslation();
    return (SP_JSX.jsxs(SP_JSX.Fragment, { children: [SP_JSX.jsx("style", { children: `
        .unifideck-account-switch + div { display: none !important; }
        .DialogFooter { display: none !important; }
      ` }), SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("accountSwitch.title"), strDescription: "", bHideCloseIcon: false, onOK: closeModal, onCancel: closeModal, children: SP_JSX.jsxs("div", { className: "unifideck-account-switch", style: { padding: "10px 0" }, children: [SP_JSX.jsx("div", { style: {
                                marginBottom: 20,
                                color: "#ccc",
                                fontSize: 14,
                                lineHeight: 1.5,
                            }, children: t("accountSwitch.description") }), SP_JSX.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: [hasRegistry && (SP_JSX.jsxs(DFL.DialogButton, { onClick: async () => {
                                        closeModal();
                                        await onMigrate();
                                    }, style: {
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: 8,
                                    }, children: [SP_JSX.jsx(FaExchangeAlt, {}), " ", t("accountSwitch.migrate")] })), hasAuthTokens && (SP_JSX.jsxs(DFL.DialogButton, { onClick: async () => {
                                        closeModal();
                                        await onClearAuths();
                                    }, style: {
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: 8,
                                        color: "#ef4444",
                                    }, children: [SP_JSX.jsx(FaTrashAlt, {}), " ", t("accountSwitch.freshStart")] })), SP_JSX.jsxs(DFL.DialogButton, { onClick: () => {
                                        closeModal();
                                        onSkip();
                                    }, style: {
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: 8,
                                        opacity: 0.7,
                                    }, children: [SP_JSX.jsx(FaForward, {}), " ", t("accountSwitch.skip")] })] })] }) })] }));
};

function formatTs(ts) {
    if (!ts)
        return "—";
    return new Date(ts * 1000).toLocaleString(undefined, {
        month: "short",
        day: "numeric",
        year: "numeric",
        hour: "numeric",
        minute: "2-digit",
    });
}
function formatBytes(n) {
    if (!n)
        return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    let value = n;
    let i = 0;
    while (value >= 1024 && i < units.length - 1) {
        value /= 1024;
        i += 1;
    }
    return `${value.toFixed(i > 0 && value < 10 ? 1 : 0)} ${units[i]}`;
}
const CloudSaveConflictModal = ({ gameTitle, local, remote, onKeepLocal, onKeepRemote, onCancel, closeModal, }) => {
    const { t } = useTranslation();
    const localNewer = local.timestamp >= remote.timestamp;
    return (SP_JSX.jsx(DFL.ConfirmModal, { strTitle: t("cloudSave.title"), strDescription: "", onOK: closeModal, onCancel: () => {
            onCancel();
            closeModal();
        }, children: SP_JSX.jsxs("div", { style: { padding: "10px 0" }, children: [SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        alignItems: "center",
                        gap: 10,
                        marginBottom: 15,
                        color: "#ffc107",
                    }, children: [SP_JSX.jsx(FaExclamationTriangle, { size: 24 }), SP_JSX.jsx("span", { style: { fontSize: 14 }, children: t("cloudSave.description", { game: gameTitle }) })] }), SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        flexDirection: "column",
                        gap: 10,
                        marginBottom: 20,
                        backgroundColor: "rgba(255,255,255,0.05)",
                        padding: 15,
                        borderRadius: 8,
                    }, children: [SP_JSX.jsxs("div", { style: {
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "space-between",
                            }, children: [SP_JSX.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx(FaDesktop, { size: 16 }), " ", SP_JSX.jsx("span", { children: t("cloudSave.local") })] }), SP_JSX.jsxs("div", { style: {
                                        display: "flex",
                                        flexDirection: "column",
                                        alignItems: "flex-end",
                                    }, children: [SP_JSX.jsxs("span", { style: {
                                                color: localNewer ? "#4caf50" : "#888",
                                                fontWeight: localNewer ? "bold" : "normal",
                                            }, children: [formatTs(local.timestamp), " ", localNewer && t("cloudSave.newer")] }), SP_JSX.jsx("span", { style: { fontSize: 12, color: "#aaa" }, children: t("cloudSave.filesSize", {
                                                count: local.file_count ?? 0,
                                                size: formatBytes(local.total_bytes ?? 0),
                                            }) })] })] }), SP_JSX.jsxs("div", { style: {
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "space-between",
                            }, children: [SP_JSX.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [SP_JSX.jsx(FaCloud, { size: 16 }), " ", SP_JSX.jsx("span", { children: t("cloudSave.cloud") })] }), SP_JSX.jsxs("div", { style: {
                                        display: "flex",
                                        flexDirection: "column",
                                        alignItems: "flex-end",
                                    }, children: [SP_JSX.jsxs("span", { style: {
                                                color: !localNewer ? "#4caf50" : "#888",
                                                fontWeight: !localNewer ? "bold" : "normal",
                                            }, children: [formatTs(remote.timestamp), " ", !localNewer && t("cloudSave.newer")] }), SP_JSX.jsx("span", { style: { fontSize: 12, color: "#aaa" }, children: t("cloudSave.filesSize", {
                                                count: remote.file_count ?? 0,
                                                size: formatBytes(remote.total_bytes ?? 0),
                                            }) })] })] })] }), SP_JSX.jsxs("div", { style: {
                        display: "flex",
                        gap: 10,
                        justifyContent: "center",
                        marginBottom: 15,
                    }, children: [SP_JSX.jsxs(DFL.DialogButton, { onClick: async () => {
                                await onKeepRemote();
                                closeModal();
                            }, style: {
                                minWidth: 140,
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                                gap: 8,
                            }, children: [SP_JSX.jsx(FaCloud, {}), " ", t("cloudSave.useCloud")] }), SP_JSX.jsxs(DFL.DialogButton, { onClick: async () => {
                                await onKeepLocal();
                                closeModal();
                            }, style: {
                                minWidth: 140,
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                                gap: 8,
                            }, children: [SP_JSX.jsx(FaDesktop, {}), " ", t("cloudSave.useLocal")] })] })] }) }));
};

/**
 * Bootstrap task : push the persisted UI language
 * to i18next and to the backend before the rest of
 * the tree mounts, so toasts and labels are already
 * localised on the very first render.
 *
 * @returns a promise that resolves once both sinks
 *   have acknowledged the new language.
 */
async function applyLanguagePreference() {
    try {
        // get_language_preference returns the {success, error, data}
        // envelope — unwrap to the {locale} payload (raw `call` doesn't).
        const raw = await call(rpcRoutes.getLanguagePreference);
        const r = unwrapRpcEnvelope(raw, {
            route: rpcRoutes.getLanguagePreference,
            throwing: false,
        });
        const pref = r?.locale || AUTO_DETECT$1;
        // "auto" (or an empty pref) resolves to the detected tag —
        // i18next has no "auto" bundle and would fall back to English.
        const tag = pref === AUTO_DETECT$1 ? resolveAutoLanguage() : pref;
        if (instance.language !== tag) {
            await instance.changeLanguage(tag);
        }
        // Mirror the preference so the early toast path and
        // <LocaleProvider> agree on what's selected.
        try {
            localStorage.setItem(LANG_STORAGE_KEY, pref);
        }
        catch {
            // ignore quota/availability errors
        }
    }
    catch {
        // Backend not ready — safe default (navigator language) stays
    }
}
/**
 * Bootstrap task : compare the active Steam user
 * with the one Unifideck last ran under. On
 * mismatch, emit `ACCOUNT_SWITCHED` and clear
 * cross-account caches so we don't leak the previous
 * user's library / artwork.
 *
 * @returns a promise resolving once the check (and
 *   any required cache clear) is done.
 */
async function checkAccountSwitch() {
    try {
        const r = await call(rpcRoutes.checkAccountSwitch);
        if (!r?.show_modal)
            return;
        DFL.showModal(SP_JSX.jsx(AccountSwitchModal, { hasRegistry: r.has_registry, hasAuthTokens: r.has_auth_tokens, onMigrate: async () => {
                await call(rpcRoutes.migrateAccountData);
                DFL.showModal(SP_JSX.jsx(SteamRestartModal, {}));
            }, onClearAuths: async () => {
                await call(rpcRoutes.clearStoreAuths);
            }, onSkip: () => { }, closeModal: () => { } }));
    }
    catch {
        // Silently ignore — never block plugin load
    }
}
/**
 * Bootstrap task : install the singleton Steam
 * lifetime listener that drives the Unifideck game
 * runner. Returns the disposer used by
 * `runTeardown` (OP-79) on plugin shutdown.
 *
 * @returns a disposer that detaches the listener.
 */
function registerLifetimeListener() {
    try {
        return (window.SteamClient?.GameSessions?.RegisterForAppLifetimeNotifications?.((n) => onAppLifetime(n)) ?? null);
    }
    catch (e) {
        console.error("[Bootstrap] lifetime listener registration failed:", e);
        return null;
    }
}
/** On app lifetime. */
function onAppLifetime(n) {
    if (n.bRunning) {
        void call(rpcRoutes.notifyGameLaunched, n.unAppID).catch(() => { });
    }
    else {
        void call(rpcRoutes.notifyGameStopped, n.unAppID).catch(() => { });
    }
}
/**
 * Bootstrap task : sweep any persistent OAuth auth
 * shortcut left over in Steam's in-memory app store by
 * a previous plugin version. Today's auth flow only
 * uses ephemeral shortcuts (created and removed in one
 * connect cycle); a stale persistent row would otherwise
 * keep launching with cover art + a real-game tile
 * instead of the unifideck-launcher tile. Idempotent —
 * RemoveShortcut on an unknown appid is a no-op.
 */
function purgeLeftoverAuthShortcuts() {
    try {
        const appStore = window.appStore;
        const map = appStore?.m_mapApps;
        if (!map?.forEach)
            return;
        const stalePrefixes = [
            "epic:epic-auth",
            "gog:gog-auth",
            "amazon:amazon-auth",
            "microsoft:ms-auth",
        ];
        const victims = [];
        map.forEach((app, appId) => {
            const lo = app?.LaunchOptions ?? app?.launch_options;
            if (typeof lo !== "string")
                return;
            if (stalePrefixes.some((p) => lo.startsWith(p))) {
                victims.push(appId);
            }
        });
        const steamApps = window.SteamClient?.Apps;
        if (!steamApps?.RemoveShortcut)
            return;
        for (const appId of victims) {
            console.log(`[Bootstrap] Removing leftover persistent auth shortcut appId=${appId}`);
            try {
                steamApps.RemoveShortcut(appId);
            }
            catch (e) {
                console.error(`[Bootstrap] RemoveShortcut(${appId}) failed:`, e);
            }
        }
    }
    catch (e) {
        console.error("[Bootstrap] purgeLeftoverAuthShortcuts failed:", e);
    }
}
/**
 * Bootstrap task : sweep orphaned Unifideck shortcuts that the
 * post-sync reconcile can't see — it keys off `LaunchOptions`,
 * never the `Exe`/target field, so a shortcut with our launcher
 * as its target but empty launch options lingers forever.
 *
 * The backend reads shortcuts.vdf (the only place that has both
 * fields) and classifies orphans; here we act on them live :
 *  - `delete` (Type A : our launcher target, no valid launch
 *    options) → `RemoveShortcut`, live, no Steam restart. These
 *    are unrecoverable (we can't tell which game they were).
 *  - `recover` (Type B : valid launch options, missing/foreign
 *    target) → logged only. The frontend has no `SetShortcutExe`
 *    to fix the target live; the next library sync's reconcile
 *    restores it (in-library) or sweeps it (out-of-library).
 *
 * Best-effort and idempotent — `RemoveShortcut` on an unknown
 * appid is a no-op, and once Steam persists the deletion a re-run
 * finds nothing.
 */
async function sweepOrphanedShortcuts() {
    try {
        const raw = await call(rpcRoutes.scanOrphanedShortcuts);
        const r = unwrapRpcEnvelope(raw, {
            route: rpcRoutes.scanOrphanedShortcuts,
            throwing: false,
        });
        if (!r)
            return;
        const toRecover = Array.isArray(r.recover) ? r.recover : [];
        for (const entry of toRecover) {
            console.log(`[Bootstrap] Orphaned shortcut with recoverable launch options ` +
                `appId=${entry.appid_unsigned} (${entry.full_id ?? "?"}) — ` +
                `target fix deferred to next library sync`);
        }
        const toDelete = Array.isArray(r.delete) ? r.delete : [];
        if (toDelete.length === 0)
            return;
        const steamApps = window.SteamClient?.Apps;
        if (!steamApps?.RemoveShortcut)
            return;
        for (const entry of toDelete) {
            const appId = entry.appid_unsigned;
            if (typeof appId !== "number")
                continue;
            console.log(`[Bootstrap] Removing orphaned shortcut appId=${appId} ` +
                `(${entry.name || "unnamed"})`);
            try {
                steamApps.RemoveShortcut(appId);
            }
            catch (e) {
                console.error(`[Bootstrap] RemoveShortcut(${appId}) failed:`, e);
            }
        }
    }
    catch (e) {
        console.error("[Bootstrap] sweepOrphanedShortcuts failed:", e);
    }
}
/** Run all bootstrap tasks concurrently. Returns the
 *  unregister handle for the lifetime listener so the
 *  plugin entry can call it on unload. */
async function runBootstrapTasks() {
    purgeLeftoverAuthShortcuts();
    // Push the live logged-in Steam user to the backend FIRST — it's the only
    // 100%-correct source of the active account, and everything below (account-
    // switch check, owned-titles, any sync) must target the right userdata dir.
    await uploadActiveSteamUser();
    const [, , , , listener] = await Promise.all([
        applyLanguagePreference(),
        checkAccountSwitch(),
        // Seed the owned-Steam-library snapshot early so a backend-triggered
        // (auto) sync can hide Steam-linked Ubisoft games before the user's
        // first manual sync. Best effort; refreshed again before each sync.
        uploadSteamOwnedTitles(),
        // Remove orphaned Unifideck shortcuts (launcher target, empty launch
        // options) that reconcile is blind to. Disjoint from the auth-shortcut
        // purge above (auth shortcuts are protected backend-side), so safe to
        // run concurrently.
        sweepOrphanedShortcuts(),
        Promise.resolve(registerLifetimeListener()),
    ]);
    return listener;
}

const POLL_INTERVAL_MS = 2000;
const DEFAULT_DURATION$1 = 5000;
const LONG_DURATION = 7500;
/**
 * Start the launcher-toast poll. Returns a stop function for teardown.
 */
function startLauncherToastPoll() {
    let stopped = false;
    const tick = async () => {
        let events;
        try {
            const raw = await call(rpcRoutes.getLauncherToasts);
            events = unwrapRpcEnvelope(raw, {
                route: rpcRoutes.getLauncherToasts,
                throwing: false,
            });
        }
        catch {
            return; // best-effort — launcher toasts are non-critical
        }
        if (!Array.isArray(events))
            return;
        for (const ev of events)
            showLauncherToast(ev);
    };
    const timer = setInterval(() => {
        if (!stopped)
            void tick();
    }, POLL_INTERVAL_MS);
    return () => {
        stopped = true;
        clearInterval(timer);
    };
}
function showLauncherToast(ev) {
    const params = (ev.i18n_params ?? {});
    // Cloud-save conflict → modal so the user can pick keep-local/remote.
    if (ev.action?.verb === "retry-sync") {
        const [store, gameId, phase] = ev.action.args;
        DFL.showModal(SP_JSX.jsx(CloudSaveConflictModal, { gameTitle: String(ev.game_title ?? gameId), local: (ev.local_snapshot ?? {}), remote: (ev.remote_snapshot ?? {}), onKeepLocal: () => {
                void EventBusClient.dispatchAction("retry-sync", store, gameId, "sync_up");
            }, onKeepRemote: () => {
                void EventBusClient.dispatchAction("retry-sync", store, gameId, phase);
            }, onCancel: () => { }, closeModal: () => { } }));
        return;
    }
    const message = ev.i18n_key ? String(instance.t(ev.i18n_key, params)) : "";
    if (!message && !ev.i18n_title_key)
        return;
    // Match the QAM ToastEventListener: title key (when present) is the
    // bold header and i18n_key is the body; otherwise the message is the
    // title with no body.
    const title = ev.i18n_title_key
        ? String(instance.t(ev.i18n_title_key, params))
        : message;
    const body = ev.i18n_title_key ? message : "";
    const longer = ev.severity === "error" || ev.severity === "warning";
    try {
        toaster.toast({
            title,
            body,
            duration: longer ? LONG_DURATION : DEFAULT_DURATION$1,
        });
    }
    catch {
        console.log(`[LauncherToast] ${title}: ${body}`);
    }
}

const DEFAULT_DURATION = 5000;
const ERROR_DURATION = 7500;
/** Show a toast via the imperative Decky toaster API. */
function showToast(title, body, severity) {
    const longer = severity === "error" || severity === "warning";
    try {
        toaster.toast({
            title,
            body,
            duration: longer ? ERROR_DURATION : DEFAULT_DURATION,
        });
    }
    catch {
        console.log(`[BootEventListener] ${title}: ${body}`);
    }
}
/**
 * Start the boot-time event listener. Returns a cleanup function
 * that unsubscribes all handlers (called from `runTeardown`).
 */
function startBootEventListener() {
    const unsubs = [];
    // ── LAUNCHER_STAGE ────────────────────────────────────
    unsubs.push(EventBusClient.subscribe("launcher_stage", (payload) => {
        const p = payload;
        const message = p.i18n_key
            ? String(instance.t(p.i18n_key, p.i18n_params))
            : "";
        if (!message)
            return;
        // Cloud-save conflict → modal
        if (p.action?.verb === "retry-sync") {
            const [store, gameId, phase] = p.action.args;
            DFL.showModal(SP_JSX.jsx(CloudSaveConflictModal, { gameTitle: String(payload.game_title ?? gameId), local: (payload.local_snapshot ??
                    {}), remote: (payload.remote_snapshot ??
                    {}), onKeepLocal: () => {
                    void EventBusClient.dispatchAction("retry-sync", store, gameId, "sync_up");
                }, onKeepRemote: () => {
                    void EventBusClient.dispatchAction("retry-sync", store, gameId, phase);
                }, onCancel: () => { }, closeModal: () => { } }));
            return;
        }
        // Generic toast
        if (p.i18n_title_key) {
            const title = String(instance.t(p.i18n_title_key, p.i18n_params));
            showToast(title, message, p.severity);
        }
        else {
            showToast(message, "", p.severity);
        }
    }));
    // ── STORE_ERROR ───────────────────────────────────────
    unsubs.push(EventBusClient.subscribe("store_error", (payload) => {
        const store = String(payload.store ?? "?");
        const errType = String(payload.error_type ?? "error");
        showToast(String(instance.t("toasts.storeError", { store, errType })), "", "error");
    }));
    // ── STORE_AUTH_COMPLETE ───────────────────────────────
    unsubs.push(EventBusClient.subscribe("store_auth_complete", () => {
        try {
            DFL.Navigation.Navigate("/library/home");
        }
        catch (e) {
            console.error("[BootEventListener] post-auth navigation failed:", e);
        }
    }));
    return () => {
        for (const unsub of unsubs)
            unsub();
    };
}

/**
 * Run every disposer captured during bootstrap, in
 * reverse registration order. Each disposer is
 * isolated so one throwing does not skip the others.
 *
 * Decky Loader calls this on plugin unload — leaks
 * here can survive across reloads of the dev cycle
 * and produce subtle phantom listeners.
 */
function runTeardown(handles) {
    // Stop boot-time singletons first (they hold EventBus
    // subscriptions and polling timers).
    try {
        syncStore.stop();
    }
    catch (e) {
        console.warn("[Teardown] syncStore stop failed:", e);
    }
    try {
        downloadStore.stop();
    }
    catch (e) {
        console.warn("[Teardown] downloadStore stop failed:", e);
    }
    try {
        authStore.stop();
    }
    catch (e) {
        console.warn("[Teardown] authStore stop failed:", e);
    }
    try {
        storeInfoStore.stop();
    }
    catch (e) {
        console.warn("[Teardown] storeInfoStore stop failed:", e);
    }
    if (handles.bootEventListener) {
        try {
            handles.bootEventListener();
        }
        catch (e) {
            console.warn("[Teardown] boot event listener stop failed:", e);
        }
    }
    if (handles.launcherToastPoll) {
        try {
            handles.launcherToastPoll();
        }
        catch (e) {
            console.warn("[Teardown] launcher toast poll stop failed:", e);
        }
    }
    if (handles.tileStoreBadgePatch) {
        try {
            handles.tileStoreBadgePatch();
        }
        catch (e) {
            console.warn("[Teardown] tile store-badge patch stop failed:", e);
        }
    }
    if (handles.appContextMenuPatch) {
        try {
            handles.appContextMenuPatch.unpatch();
        }
        catch (e) {
            console.warn("[Teardown] app context-menu patch unpatch failed:", e);
        }
    }
    if (handles.lifetimeListener) {
        try {
            handles.lifetimeListener.unregister();
        }
        catch (e) {
            console.warn("[Teardown] lifetime listener unregister failed:", e);
        }
    }
    if (handles.appStorePatch) {
        try {
            handles.appStorePatch.remove();
        }
        catch (e) {
            console.warn("[Teardown] app-store patch remove failed:", e);
        }
    }
    if (handles.collectionManager) {
        try {
            handles.collectionManager.remove();
        }
        catch (e) {
            console.warn("[Teardown] collection manager remove failed:", e);
        }
    }
    if (handles.libraryPatch) {
        try {
            handles.libraryPatch.remove();
        }
        catch (e) {
            console.warn("[Teardown] library patch remove failed:", e);
        }
    }
    if (handles.routerPatch) {
        try {
            handles.routerPatch.remove();
        }
        catch (e) {
            console.warn("[Teardown] router patch remove failed:", e);
        }
    }
}

// Eager translation load — Decky's UI mounts before any
// async work resolves, so we kick this off at module import.
void initI18n();
/** Panel content. */
const PanelContent = () => (SP_JSX.jsx(RootProvider, { children: SP_JSX.jsx(QuickAccessPanel, {}) }));
var index = definePlugin(() => {
    console.log("[Unifideck] Plugin loaded");
    const bridge = new SteamBridge();
    const handles = {};
    // Apply the App Details patch immediately so the very
    // first navigation to a game's page shows our overrides.
    try {
        handles.routerPatch = applyAppDetailsPatch(bridge);
    }
    catch (e) {
        console.error("[Unifideck] router patch failed:", e);
    }
    // Populate the unifideck game cache before patching tabs so
    // filters have data on first render — independent of whether
    // the user ever opens the Decky QAM panel.
    try {
        startUnifideckCacheAutoload();
    }
    catch (e) {
        console.error("[Unifideck] cache autoload start failed:", e);
    }
    // Load ProtonDB compat cache at boot so library tab patches
    // have compat badges on first render (no QAM open required).
    void loadCompatCacheFromBackend();
    // Inject the custom Unifideck tabs into Steam's library via
    // the ``/library`` route patch — same primitive TabMaster uses
    // (works in both Desktop and Gaming Mode).
    try {
        handles.libraryPatch = applyLibraryPatch(bridge);
    }
    catch (e) {
        console.error("[Unifideck] library patch failed:", e);
    }
    // Auto-generate [Unifideck] Steam Collections + keep them
    // in sync with the tab filters on every library sync.
    try {
        handles.collectionManager = startCollectionManager();
    }
    catch (e) {
        console.error("[Unifideck] collection manager start failed:", e);
    }
    // Enrich non-Steam shortcut AppOverviews (metacritic, deck compat,
    // store categories, release date, reviews, date-added, playtime,
    // size) so Steam's NATIVE library Sort menu + Library Filters work
    // for them. Runs at boot — NOT on QAM mount — so it works in Gaming
    // Mode, and re-applies on every overview re-set so closing a game's
    // details page doesn't wipe the enrichment.
    try {
        handles.overviewEnrichment = startOverviewEnrichment();
    }
    catch (e) {
        console.error("[Unifideck] overview enrichment start failed:", e);
    }
    // Render each non-Steam shortcut's STORE logo (Epic/GOG/…) on its
    // library tile in place of the Steam Deck 'D' glyph, keeping the
    // compat status badge. Boot-time (before the grid mounts) so it works
    // in Gaming Mode. Read-only w.r.t. overviews → launch unaffected.
    try {
        handles.tileStoreBadgePatch = startTileStoreBadgePatch();
    }
    catch (e) {
        console.error("[Unifideck] tile store-badge patch start failed:", e);
    }
    // Inject "Change executable…" into the native game context menu for
    // installed Unifideck shortcuts (gog/amazon/epic). Read-only w.r.t. the
    // overview — it only ADDS a menu item, never touches launch routing.
    try {
        handles.appContextMenuPatch = applyAppContextMenuPatch();
    }
    catch (e) {
        console.error("[Unifideck] app context-menu patch start failed:", e);
    }
    // ── Boot-time singletons ──────────────────────────────
    // Start all reactive stores at boot so they track state
    // even when the QAM panel is closed. Each store subscribes
    // to EventBus events and/or fetches initial data.
    authStore.start();
    storeInfoStore.start();
    downloadStore.start();
    syncStore.start();
    // Boot-time event listener — handles STORE_ERROR, LAUNCHER_STAGE,
    // and STORE_AUTH_COMPLETE events independently of QAM mount.
    try {
        handles.bootEventListener = startBootEventListener();
    }
    catch (e) {
        console.error("[Unifideck] boot event listener start failed:", e);
    }
    // Persistent poll for launcher-subprocess toasts (first-time prefix
    // setup, dependency install, Proton switch). Runs here — NOT in the
    // QAM panel — so launch-time toasts show in Gaming Mode where the
    // panel is closed during the launch.
    try {
        handles.launcherToastPoll = startLauncherToastPoll();
    }
    catch (e) {
        console.error("[Unifideck] launcher toast poll start failed:", e);
    }
    // Spoof non-Steam Unifideck shortcuts as Steam Store games so
    // Steam's own UI surfaces (library tile, AppDetails page,
    // friend presence) render real cover art + descriptions instead
    // of the bare shortcut skeleton. Async — needs to fetch the
    // mappings + appdetails caches from the backend before patching.
    void applyAppStorePatch()
        .then((handle) => {
        handles.appStorePatch = handle;
    })
        .catch((e) => {
        console.error("[Unifideck] app-store patch failed:", e);
    });
    // Bootstrap tasks (language, account switch, lifetime
    // listener) run async ; the lifetime listener handle is
    // captured for teardown when its promise resolves.
    void runBootstrapTasks().then((listener) => {
        handles.lifetimeListener = listener;
    });
    return {
        name: "Unifideck",
        titleView: SP_JSX.jsx("div", { children: "Unifideck" }),
        content: SP_JSX.jsx(PanelContent, {}),
        icon: SP_JSX.jsx(FaGamepad, {}),
        onDismount: () => {
            console.log("[Unifideck] Plugin unloading");
            runTeardown(handles);
        },
    };
});

export { index as default };
//# sourceMappingURL=index.js.map
