koin.js 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
1
  'use strict';
2
2
 
3
- var React5 = require('react');
3
+ var React2 = require('react');
4
4
  var lucideReact = require('lucide-react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var nostalgist = require('nostalgist');
7
7
 
8
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
 
10
- var React5__default = /*#__PURE__*/_interopDefault(React5);
10
+ var React2__default = /*#__PURE__*/_interopDefault(React2);
11
11
 
12
12
  var __defProp = Object.defineProperty;
13
13
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14
14
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
15
- var ControlButton = React5.memo(function ControlButton2({
15
+ var ControlButton = React2.memo(function ControlButton2({
16
16
  onClick,
17
17
  onMouseDown,
18
18
  onMouseUp,
@@ -39,31 +39,45 @@ var ControlButton = React5.memo(function ControlButton2({
39
39
  onTouchEnd,
40
40
  disabled,
41
41
  className: `
42
- group relative flex flex-col items-center gap-1 px-2 sm:px-3 py-2 rounded-lg
43
- transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed
44
- select-none flex-shrink-0
45
- ${active ? "" : danger ? "hover:bg-red-500/20 text-gray-400 hover:text-red-400" : "hover:bg-white/10 text-gray-400 hover:text-white"}
46
- ${className}
47
- `,
42
+ flex flex-col items-center justify-center gap-1.5
43
+ px-3 py-2 rounded-lg
44
+ transition-all duration-200
45
+ disabled:opacity-50 disabled:cursor-not-allowed
46
+ hover:bg-white/10 active:bg-white/20
47
+ ${active ? "bg-white/10 ring-1 ring-inset" : ""}
48
+ ${danger ? "hover:bg-red-500/20 text-red-400" : "text-gray-400 hover:text-white"}
49
+ ${className}
50
+ `,
48
51
  style: active ? {
49
52
  backgroundColor: `${systemColor}20`,
50
53
  color: systemColor
51
54
  } : {},
52
55
  title: label,
53
56
  children: [
54
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: iconSize, className: "transition-transform group-hover:scale-110" }),
55
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[9px] font-bold uppercase tracking-wider opacity-70", children: label })
57
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: iconSize, className: `transition-transform duration-200 ${active ? "scale-110" : "group-hover:scale-110"}`, style: active ? { color: systemColor } : void 0 }),
58
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[9px] font-bold uppercase tracking-wider whitespace-nowrap", style: { color: active ? systemColor : void 0 }, children: label })
56
59
  ]
57
60
  }
58
61
  );
59
62
  });
60
63
  var SPEED_OPTIONS = [1, 2];
61
64
  function SpeedMenu({ speed, onSpeedChange, disabled = false }) {
62
- const [showMenu, setShowMenu] = React5.useState(false);
65
+ const [showMenu, setShowMenu] = React2.useState(false);
66
+ const buttonRef = React2__default.default.useRef(null);
67
+ const getMenuPosition = () => {
68
+ if (!buttonRef.current) return {};
69
+ const rect = buttonRef.current.getBoundingClientRect();
70
+ return {
71
+ bottom: `${window.innerHeight - rect.top + 8}px`,
72
+ left: `${rect.left + rect.width / 2}px`,
73
+ transform: "translateX(-50%)"
74
+ };
75
+ };
63
76
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
64
77
  /* @__PURE__ */ jsxRuntime.jsxs(
65
78
  "button",
66
79
  {
80
+ ref: buttonRef,
67
81
  onClick: () => setShowMenu(!showMenu),
68
82
  disabled,
69
83
  className: `
@@ -90,25 +104,32 @@ function SpeedMenu({ speed, onSpeedChange, disabled = false }) {
90
104
  }
91
105
  ),
92
106
  showMenu && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
93
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-40", onClick: () => setShowMenu(false) }),
94
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-black/90 backdrop-blur-md border border-white/20 rounded-lg p-1.5 shadow-xl z-50 flex flex-col gap-1 min-w-[80px]", children: SPEED_OPTIONS.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
95
- "button",
107
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-[9998]", onClick: () => setShowMenu(false) }),
108
+ /* @__PURE__ */ jsxRuntime.jsx(
109
+ "div",
96
110
  {
97
- onClick: () => {
98
- onSpeedChange(s);
99
- setShowMenu(false);
100
- },
101
- className: `
111
+ className: "fixed z-[9999] bg-black/90 backdrop-blur-md border border-white/20 rounded-lg p-1.5 shadow-xl flex flex-col gap-1 min-w-[80px]",
112
+ style: getMenuPosition(),
113
+ children: SPEED_OPTIONS.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
114
+ "button",
115
+ {
116
+ onClick: () => {
117
+ onSpeedChange(s);
118
+ setShowMenu(false);
119
+ },
120
+ className: `
102
121
  px-3 py-2 rounded text-xs font-mono font-bold text-center transition-colors
103
122
  ${speed === s ? "bg-white/20 text-white" : "hover:bg-white/10 text-gray-400 hover:text-white"}
104
123
  `,
105
- children: [
106
- s,
107
- "x"
108
- ]
109
- },
110
- s
111
- )) })
124
+ children: [
125
+ s,
126
+ "x"
127
+ ]
128
+ },
129
+ s
130
+ ))
131
+ }
132
+ )
112
133
  ] })
113
134
  ] });
114
135
  }
@@ -177,7 +198,261 @@ function HardcoreTooltip({ show, message = "Disabled in Hardcore mode", children
177
198
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-amber-500/90 text-black text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50", children: message })
178
199
  ] });
179
200
  }
180
- var PlaybackControls = React5.memo(function PlaybackControls2({
201
+
202
+ // src/locales/en.ts
203
+ var en = {
204
+ controls: {
205
+ play: "Play",
206
+ pause: "Pause",
207
+ reset: "Reset",
208
+ rewind: "Rewind",
209
+ save: "Save",
210
+ load: "Load",
211
+ snap: "Snap",
212
+ rec: "Rec",
213
+ stopRec: "Stop",
214
+ startRecord: "Start recording",
215
+ stopRecord: "Stop recording",
216
+ mute: "Mute",
217
+ unmute: "Unmute",
218
+ help: "Help",
219
+ full: "Full",
220
+ keys: "Keys",
221
+ menuOpen: "Open menu",
222
+ menuClose: "Close menu",
223
+ gamepadConnected: "{{count}} controller{{plural}} connected - click to configure",
224
+ noGamepad: "No controller detected - press any button on your gamepad to connect",
225
+ press: "Press",
226
+ startBtn: "START",
227
+ selectBtn: "SEL"
228
+ },
229
+ common: {
230
+ disabledInHardcore: "Disabled in Hardcore mode",
231
+ notSupported: "Not supported on this console",
232
+ playToEnableRewind: "Play for a few seconds to enable rewind"
233
+ },
234
+ settings: {
235
+ title: "Settings",
236
+ general: "General",
237
+ audio: "Audio",
238
+ video: "Video",
239
+ input: "Input",
240
+ advanced: "Advanced",
241
+ fullscreen: "Fullscreen",
242
+ controls: "Controls",
243
+ gamepad: "Gamepad",
244
+ cheats: "Cheats",
245
+ retroAchievements: "RetroAchievements",
246
+ shortcuts: "Shortcuts",
247
+ exit: "Exit",
248
+ language: "Language",
249
+ selectLanguage: "Select Language"
250
+ },
251
+ overlay: {
252
+ play: "PLAY",
253
+ systemFirmware: "SYSTEM FIRMWARE",
254
+ loading: "Loading {{system}}",
255
+ initializing: "Initializing emulator",
256
+ loadingSave: "Loading Save",
257
+ preparingSlot: "Preparing slot {{num}}",
258
+ systemError: "System Error",
259
+ failedInit: "Failed to initialize emulator",
260
+ retry: "Retry",
261
+ slotReady: "Slot {{num}} ready",
262
+ paused: "Paused"
263
+ },
264
+ notifications: {
265
+ saved: "State saved",
266
+ loaded: "State loaded",
267
+ error: "Error",
268
+ recordingStarted: "Recording started",
269
+ recordingSaved: "Recording saved",
270
+ downloaded: "State downloaded",
271
+ loadedFile: "State loaded from file",
272
+ savedSlot: "Saved to slot {{num}}",
273
+ loadedSlot: "Loaded from slot {{num}}",
274
+ deletedSlot: "Deleted slot {{num}}",
275
+ emptySlot: "Empty slot",
276
+ noSaveFound: "No save found",
277
+ failedSave: "Failed to save",
278
+ failedLoad: "Failed to load",
279
+ failedDelete: "Failed to delete",
280
+ failedFetch: "Failed to load save slots",
281
+ controllerConnected: "Controller Connected",
282
+ controllerDisconnected: "Controller Lost",
283
+ controllerReady: "Controller ready to use",
284
+ insertCoin: "Press SHIFT to insert coin",
285
+ insertCoinTitle: "\u{1FA99} Insert Coin",
286
+ controlsSaved: "Controls saved",
287
+ controlsReset: "Controls reset to defaults"
288
+ },
289
+ modals: {
290
+ shortcuts: {
291
+ title: "Keyboard Shortcuts",
292
+ playerShortcuts: "Player Shortcuts",
293
+ overlays: "Overlays",
294
+ recording: "Recording",
295
+ showHelp: "Show Help",
296
+ perfOverlay: "Performance Overlay",
297
+ inputDisplay: "Input Display",
298
+ toggleRec: "Toggle Recording",
299
+ toggleMute: "Toggle Mute",
300
+ pressEsc: "Press ESC to close"
301
+ },
302
+ controls: {
303
+ title: "Controls",
304
+ keyboard: "Keyboard Mapping",
305
+ description: "Click a button and press a key to remap",
306
+ pressKey: "Press...",
307
+ reset: "Reset to Default",
308
+ save: "Save Controls"
309
+ },
310
+ gamepad: {
311
+ title: "Gamepad Settings",
312
+ noGamepad: "No gamepad detected",
313
+ connected: "{{count}} controller{{s}} connected",
314
+ none: "No controllers detected",
315
+ player: "Player:",
316
+ noController: "No controller detected",
317
+ pressAny: "Press any button on your gamepad to connect",
318
+ waiting: "Waiting for input...",
319
+ pressButton: "Press a button on your controller for {{button}}",
320
+ pressEsc: "Press Escape to cancel",
321
+ reset: "Reset to Default",
322
+ save: "Save Settings"
323
+ },
324
+ cheats: {
325
+ title: "Cheats",
326
+ addCheat: "Add Cheat",
327
+ available: "{{count}} cheat{{s}} available",
328
+ emptyTitle: "No cheats available",
329
+ emptyDesc: "No cheat codes found for this game",
330
+ copy: "Copy code",
331
+ active: "{{count}} cheat{{s}} active",
332
+ toggleHint: "Click a cheat to toggle it on/off"
333
+ },
334
+ saveSlots: {
335
+ title: "Save States",
336
+ saveTitle: "Save Game",
337
+ loadTitle: "Load Game",
338
+ emptySlot: "Empty Slot",
339
+ subtitleSave: "Choose a slot to save your progress",
340
+ subtitleLoad: "Select a save to restore",
341
+ loading: "Loading saves...",
342
+ locked: "Slot {{num}} Locked",
343
+ upgrade: "Upgrade to unlock more save slots",
344
+ autoSave: "Auto-Save Slot",
345
+ autoSaveDesc: "Reserved for automatic saves",
346
+ noData: "No save data",
347
+ slot: "Slot {{num}}",
348
+ footerSave: "Saves are stored in the cloud and sync across devices",
349
+ footerLoad: "Your progress will be restored to the selected save point"
350
+ },
351
+ bios: {
352
+ title: "BIOS Selection",
353
+ description: "Select a BIOS file to use for this game",
354
+ warningTitle: "Note:",
355
+ warning: "Changing BIOS requires the emulator to restart. Your unsaved progress will be lost.",
356
+ systemDefault: "System Default",
357
+ active: "Active",
358
+ defaultDesc: "Use the emulator's built-in or default BIOS",
359
+ emptyTitle: "No BIOS files found for this console.",
360
+ emptyDesc: "Upload BIOS files in your Dashboard Library.",
361
+ footer: "System Firmware Settings"
362
+ }
363
+ },
364
+ retroAchievements: {
365
+ title: "RetroAchievements",
366
+ login: "Login",
367
+ logout: "Logout",
368
+ username: "Username",
369
+ password: "Password",
370
+ hardcore: "Hardcore Mode",
371
+ achievements: "Achievements",
372
+ locked: "Locked",
373
+ unlocked: "Unlocked",
374
+ mastered: "Mastered",
375
+ identifying: "Identifying game...",
376
+ achievementsAvailable: "{{count}} achievements available",
377
+ gameNotSupported: "Connected - Game not in RA database",
378
+ connect: "Connect RetroAchievements",
379
+ connected: "RetroAchievements (connected)",
380
+ createAccount: "Create an account",
381
+ privacy: "Privacy:",
382
+ privacyText: "Your password is only used to authenticate with RetroAchievements. It is never stored.",
383
+ connecting: "Connecting...",
384
+ connectAccount: "Connect your account to track achievements",
385
+ poweredBy: "Powered by",
386
+ connectedStatus: "Connected",
387
+ yourUsername: "Your RA username",
388
+ yourPassword: "Your RA password",
389
+ usernameRequired: "Username and Password are required",
390
+ noGame: "No game loaded",
391
+ loadGame: "Load a game to see achievements",
392
+ noAchievements: "No achievements found",
393
+ notSupported: "This game may not be supported",
394
+ ptsRemaining: "{{count}} pts remaining",
395
+ filters: {
396
+ all: "All",
397
+ locked: "Locked",
398
+ unlocked: "Unlocked"
399
+ }
400
+ },
401
+ overlays: {
402
+ performance: {
403
+ title: "Stats",
404
+ fps: "FPS",
405
+ frameTime: "FT",
406
+ memory: "MEM",
407
+ core: "Core",
408
+ input: "INPUT",
409
+ active: "ACTIVE"
410
+ },
411
+ toast: {
412
+ saved: "Game Saved",
413
+ loaded: "Game Loaded",
414
+ error: "Error"
415
+ },
416
+ recording: {
417
+ started: "Recording Started",
418
+ stopped: "Recording Stopped",
419
+ saved: "Recording Saved",
420
+ paused: "PAUSED",
421
+ recording: "RECORDING",
422
+ resume: "Resume recording",
423
+ pause: "Pause recording",
424
+ stop: "Stop and save recording",
425
+ hover: "Hover for controls"
426
+ }
427
+ }
428
+ };
429
+ var KoinI18nContext = React2.createContext(en);
430
+ function deepMerge(target, source) {
431
+ const result = { ...target };
432
+ for (const key in source) {
433
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
434
+ const sourceValue = source[key];
435
+ const targetValue = result[key];
436
+ if (sourceValue && typeof sourceValue === "object" && targetValue && typeof targetValue === "object" && !Array.isArray(sourceValue)) {
437
+ result[key] = deepMerge(targetValue, sourceValue);
438
+ } else if (sourceValue !== void 0) {
439
+ result[key] = sourceValue;
440
+ }
441
+ }
442
+ }
443
+ return result;
444
+ }
445
+ var KoinI18nProvider = ({ children, translations }) => {
446
+ const value = React2.useMemo(() => {
447
+ if (!translations) return en;
448
+ return deepMerge(en, translations);
449
+ }, [translations]);
450
+ return /* @__PURE__ */ jsxRuntime.jsx(KoinI18nContext.Provider, { value, children });
451
+ };
452
+ function useKoinTranslation() {
453
+ return React2.useContext(KoinI18nContext);
454
+ }
455
+ var PlaybackControls = React2.memo(function PlaybackControls2({
181
456
  isPaused,
182
457
  isRunning,
183
458
  speed,
@@ -196,20 +471,30 @@ var PlaybackControls = React5.memo(function PlaybackControls2({
196
471
  systemColor = "#00FF41",
197
472
  hardcoreRestrictions
198
473
  }) {
474
+ const t = useKoinTranslation();
199
475
  const hasRewindHistory = rewindBufferSize > 0;
200
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [
476
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-4 w-full sm:w-auto sm:flex-nowrap sm:gap-3 flex-shrink-0", children: [
201
477
  /* @__PURE__ */ jsxRuntime.jsx(
202
478
  ControlButton,
203
479
  {
204
480
  onClick: onPauseToggle,
205
481
  icon: !isRunning || isPaused ? lucideReact.Play : lucideReact.Pause,
206
- label: !isRunning || isPaused ? "Play" : "Pause",
482
+ label: !isRunning || isPaused ? t.controls.play : t.controls.pause,
207
483
  active: isPaused,
208
484
  disabled,
209
485
  systemColor
210
486
  }
211
487
  ),
212
- /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onRestart, icon: lucideReact.RotateCcw, label: "Reset", disabled, systemColor }),
488
+ /* @__PURE__ */ jsxRuntime.jsx(
489
+ ControlButton,
490
+ {
491
+ onClick: onRestart,
492
+ icon: lucideReact.RotateCcw,
493
+ label: t.controls.reset,
494
+ disabled,
495
+ systemColor
496
+ }
497
+ ),
213
498
  /* @__PURE__ */ jsxRuntime.jsx(SpeedMenu, { speed, onSpeedChange, disabled }),
214
499
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative group", children: [
215
500
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -221,7 +506,7 @@ var PlaybackControls = React5.memo(function PlaybackControls2({
221
506
  onTouchStart: hasRewindHistory && hardcoreRestrictions?.canUseRewind !== false ? onRewindStart : void 0,
222
507
  onTouchEnd: hasRewindHistory && hardcoreRestrictions?.canUseRewind !== false ? onRewindStop : void 0,
223
508
  icon: lucideReact.Rewind,
224
- label: "Rewind",
509
+ label: t.controls.rewind,
225
510
  active: isRewinding,
226
511
  disabled: disabled || !hasRewindHistory || hardcoreRestrictions?.canUseRewind === false,
227
512
  systemColor
@@ -232,10 +517,10 @@ var PlaybackControls = React5.memo(function PlaybackControls2({
232
517
  HardcoreTooltip,
233
518
  {
234
519
  show: hardcoreRestrictions?.canUseRewind === false,
235
- message: hardcoreRestrictions?.isHardcore ? "Disabled in Hardcore mode" : "Not supported on this console"
520
+ message: hardcoreRestrictions?.isHardcore ? t.common.disabledInHardcore : t.common.notSupported
236
521
  }
237
522
  ),
238
- hardcoreRestrictions?.canUseRewind !== false && !hasRewindHistory && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-black/90 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50", children: "Play for a few seconds to enable rewind" })
523
+ hardcoreRestrictions?.canUseRewind !== false && !hasRewindHistory && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-black/90 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50", children: t.common.playToEnableRewind })
239
524
  ] }),
240
525
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-8 bg-white/10 mx-1" }),
241
526
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -260,10 +545,10 @@ function AutoSaveIndicator({
260
545
  isPaused = false,
261
546
  onClick
262
547
  }) {
263
- const prevStateRef = React5.useRef(state);
264
- const [displayProgress, setDisplayProgress] = React5.useState(progress);
265
- const [iconOpacity, setIconOpacity] = React5.useState(1);
266
- React5.useEffect(() => {
548
+ const prevStateRef = React2.useRef(state);
549
+ const [displayProgress, setDisplayProgress] = React2.useState(progress);
550
+ const [iconOpacity, setIconOpacity] = React2.useState(1);
551
+ React2.useEffect(() => {
267
552
  const prevState = prevStateRef.current;
268
553
  if (state === "done" && prevState === "saving") {
269
554
  const startProgress = displayProgress;
@@ -306,7 +591,7 @@ function AutoSaveIndicator({
306
591
  }
307
592
  prevStateRef.current = state;
308
593
  }, [state, progress]);
309
- React5.useEffect(() => {
594
+ React2.useEffect(() => {
310
595
  if (state !== prevStateRef.current) {
311
596
  setIconOpacity(0);
312
597
  const timer = setTimeout(() => {
@@ -421,7 +706,7 @@ function AutoSaveIndicator({
421
706
  }
422
707
  );
423
708
  }
424
- var SaveLoadControls = React5.memo(function SaveLoadControls2({
709
+ var SaveLoadControls = React2.memo(function SaveLoadControls2({
425
710
  onSave,
426
711
  onLoad,
427
712
  onScreenshot,
@@ -438,7 +723,8 @@ var SaveLoadControls = React5.memo(function SaveLoadControls2({
438
723
  autoSavePaused = false,
439
724
  onAutoSaveToggle
440
725
  }) {
441
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 sm:px-4 border-x border-white/10 flex-shrink-0", children: [
726
+ const t = useKoinTranslation();
727
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-4 w-full sm:w-auto sm:flex-nowrap sm:gap-3 px-3 sm:px-4 sm:border-x sm:border-white/10 flex-shrink-0", children: [
442
728
  autoSaveEnabled && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
443
729
  AutoSaveIndicator,
444
730
  {
@@ -455,12 +741,18 @@ var SaveLoadControls = React5.memo(function SaveLoadControls2({
455
741
  {
456
742
  onClick: hardcoreRestrictions?.canUseSaveStates === false ? void 0 : onSave,
457
743
  icon: lucideReact.Save,
458
- label: "Save",
744
+ label: t.controls.save,
459
745
  disabled: disabled || saveDisabled || hardcoreRestrictions?.canUseSaveStates === false,
460
746
  systemColor
461
747
  }
462
748
  ),
463
- /* @__PURE__ */ jsxRuntime.jsx(HardcoreTooltip, { show: hardcoreRestrictions?.canUseSaveStates === false })
749
+ /* @__PURE__ */ jsxRuntime.jsx(
750
+ HardcoreTooltip,
751
+ {
752
+ show: hardcoreRestrictions?.canUseSaveStates === false,
753
+ message: hardcoreRestrictions?.isHardcore ? t.common.disabledInHardcore : t.common.notSupported
754
+ }
755
+ )
464
756
  ] }),
465
757
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative group flex-shrink-0", children: [
466
758
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -468,19 +760,25 @@ var SaveLoadControls = React5.memo(function SaveLoadControls2({
468
760
  {
469
761
  onClick: hardcoreRestrictions?.canUseSaveStates === false ? void 0 : onLoad,
470
762
  icon: lucideReact.Download,
471
- label: "Load",
763
+ label: t.controls.load,
472
764
  disabled: disabled || loadDisabled || hardcoreRestrictions?.canUseSaveStates === false,
473
765
  systemColor
474
766
  }
475
767
  ),
476
- /* @__PURE__ */ jsxRuntime.jsx(HardcoreTooltip, { show: hardcoreRestrictions?.canUseSaveStates === false })
768
+ /* @__PURE__ */ jsxRuntime.jsx(
769
+ HardcoreTooltip,
770
+ {
771
+ show: hardcoreRestrictions?.canUseSaveStates === false,
772
+ message: hardcoreRestrictions?.isHardcore ? t.common.disabledInHardcore : t.common.notSupported
773
+ }
774
+ )
477
775
  ] }),
478
776
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
479
777
  ControlButton,
480
778
  {
481
779
  onClick: onScreenshot,
482
780
  icon: lucideReact.Camera,
483
- label: "Snap",
781
+ label: t.controls.snap,
484
782
  disabled: disabled || saveDisabled,
485
783
  systemColor
486
784
  }
@@ -491,7 +789,7 @@ var SaveLoadControls = React5.memo(function SaveLoadControls2({
491
789
  onClick: onRecordToggle,
492
790
  disabled,
493
791
  className: `flex flex-col items-center gap-1 px-2 sm:px-3 py-2 rounded-lg transition-all duration-200 hover:bg-white/10 ${disabled ? "opacity-50 cursor-not-allowed" : ""}`,
494
- title: isRecording ? "Stop recording" : "Start recording",
792
+ title: isRecording ? t.controls.stopRecord : t.controls.startRecord,
495
793
  children: [
496
794
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: isRecording ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
497
795
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, { size: 20, className: "text-red-500" }),
@@ -502,7 +800,7 @@ var SaveLoadControls = React5.memo(function SaveLoadControls2({
502
800
  {
503
801
  className: "text-[10px] font-bold uppercase tracking-wider",
504
802
  style: { color: isRecording ? "#FF3333" : void 0 },
505
- children: isRecording ? "REC" : "Rec"
803
+ children: t.controls.rec
506
804
  }
507
805
  )
508
806
  ]
@@ -517,17 +815,17 @@ var QUICK_PRESETS = [
517
815
  { id: "crt/crt-geom", label: "CRT Geom" },
518
816
  { id: "handheld/lcd-grid-v2", label: "LCD Grid" }
519
817
  ];
520
- var ShaderDropdown = React5.memo(function ShaderDropdown2({
818
+ var ShaderDropdown = React2.memo(function ShaderDropdown2({
521
819
  currentShader = "",
522
820
  onShaderChange,
523
821
  isRunning = false,
524
822
  systemColor = "#00FF41",
525
823
  disabled = false
526
824
  }) {
527
- const [isOpen, setIsOpen] = React5.useState(false);
528
- const [pendingShader, setPendingShader] = React5.useState(null);
529
- const dropdownRef = React5.useRef(null);
530
- React5.useEffect(() => {
825
+ const [isOpen, setIsOpen] = React2.useState(false);
826
+ const [pendingShader, setPendingShader] = React2.useState(null);
827
+ const dropdownRef = React2.useRef(null);
828
+ React2.useEffect(() => {
531
829
  const handleClickOutside = (e) => {
532
830
  if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
533
831
  setIsOpen(false);
@@ -635,6 +933,9 @@ function RAButton({
635
933
  achievementCount,
636
934
  className = ""
637
935
  }) {
936
+ const t = useKoinTranslation();
937
+ const title = isGameFound ? `${t.retroAchievements.title} (${achievementCount} achievements)` : isConnected ? t.retroAchievements.connected : t.retroAchievements.title;
938
+ const tooltip = isIdentifying ? t.retroAchievements.identifying : isGameFound ? t.retroAchievements.achievementsAvailable.replace("{{count}}", achievementCount.toString()) : isConnected ? t.retroAchievements.gameNotSupported : t.retroAchievements.connect;
638
939
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative group ${className}`, children: [
639
940
  /* @__PURE__ */ jsxRuntime.jsxs(
640
941
  "button",
@@ -647,7 +948,7 @@ function RAButton({
647
948
  select-none
648
949
  ${isGameFound ? "bg-gradient-to-b from-yellow-500/30 to-orange-500/20 text-yellow-400 ring-1 ring-yellow-500/50 shadow-[0_0_12px_rgba(234,179,8,0.3)]" : isConnected ? "bg-yellow-500/10 text-yellow-400/70 ring-1 ring-yellow-500/30" : "hover:bg-white/10 text-gray-400 hover:text-white"}
649
950
  `,
650
- title: isGameFound ? `RetroAchievements (${achievementCount} achievements)` : isConnected ? "RetroAchievements (connected)" : "RetroAchievements",
951
+ title,
651
952
  children: [
652
953
  isIdentifying ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 20, className: "animate-spin text-yellow-400" }) : /* @__PURE__ */ jsxRuntime.jsx(
653
954
  lucideReact.Trophy,
@@ -668,13 +969,14 @@ function RAButton({
668
969
  absolute -top-1 -right-1 w-3 h-3 rounded-full border-2 border-black
669
970
  ${isGameFound ? "bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.8)]" : "bg-yellow-500 shadow-[0_0_6px_rgba(234,179,8,0.6)]"}
670
971
  `, children: isGameFound && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-green-400 rounded-full animate-ping opacity-50" }) }),
671
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900/95 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 border border-white/10", children: isIdentifying ? "Identifying game..." : isGameFound ? `${achievementCount} achievements available` : isConnected ? "Connected - Game not in RA database" : "Connect RetroAchievements" })
972
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900/95 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 border border-white/10", children: tooltip })
672
973
  ] });
673
974
  }
674
- var SettingsControls = React5.memo(function SettingsControls2({
975
+ var SettingsControls = React2.memo(function SettingsControls2({
675
976
  onFullscreen,
676
977
  onControls,
677
978
  onGamepadSettings,
979
+ onSettings,
678
980
  onCheats,
679
981
  onRetroAchievements,
680
982
  onShowShortcuts,
@@ -691,8 +993,10 @@ var SettingsControls = React5.memo(function SettingsControls2({
691
993
  raAchievementCount = 0,
692
994
  raIsIdentifying = false
693
995
  }) {
996
+ const t = useKoinTranslation();
694
997
  const gamepadIndicatorText = gamepadCount > 0 ? Array.from({ length: gamepadCount }, (_, i) => `P${i + 1}`).join(" ") : "";
695
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [
998
+ const gamepadConnectedTitle = t.controls.gamepadConnected.replace("{{count}}", gamepadCount.toString()).replace("{{plural}}", gamepadCount > 1 ? "s" : "");
999
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-4 w-full sm:w-auto sm:flex-nowrap sm:gap-3 flex-shrink-0", children: [
696
1000
  /* @__PURE__ */ jsxRuntime.jsx(
697
1001
  ShaderDropdown_default,
698
1002
  {
@@ -703,15 +1007,15 @@ var SettingsControls = React5.memo(function SettingsControls2({
703
1007
  disabled
704
1008
  }
705
1009
  ),
706
- onShowShortcuts && /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onShowShortcuts, icon: lucideReact.HelpCircle, label: "Help", disabled, className: "hidden sm:flex", systemColor }),
707
- /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onFullscreen, icon: lucideReact.Maximize, label: "Full", disabled, className: "hidden sm:flex", systemColor }),
708
- /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onControls, icon: lucideReact.Gamepad2, label: "Keys", disabled, className: "hidden sm:flex", systemColor }),
1010
+ onShowShortcuts && /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onShowShortcuts, icon: lucideReact.HelpCircle, label: t.controls.help, disabled, className: "hidden sm:flex", systemColor }),
1011
+ /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onFullscreen, icon: lucideReact.Maximize, label: t.controls.full, disabled, className: "hidden sm:flex", systemColor }),
1012
+ /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onControls, icon: lucideReact.Gamepad2, label: t.controls.keys, disabled, className: "hidden sm:flex", systemColor }),
709
1013
  gamepadCount > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
710
1014
  "button",
711
1015
  {
712
1016
  onClick: onGamepadSettings,
713
1017
  className: "relative group flex flex-col items-center gap-1 px-2 sm:px-3 py-2 rounded-lg transition-all duration-200 hover:bg-white/10 flex-shrink-0 hidden sm:flex",
714
- title: `${gamepadCount} controller${gamepadCount > 1 ? "s" : ""} connected - click to configure`,
1018
+ title: gamepadConnectedTitle,
715
1019
  children: [
716
1020
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
717
1021
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 20, style: { color: systemColor }, className: "transition-transform group-hover:scale-110" }),
@@ -732,15 +1036,15 @@ var SettingsControls = React5.memo(function SettingsControls2({
732
1036
  "button",
733
1037
  {
734
1038
  onClick: onGamepadSettings,
735
- className: "relative group flex-col items-center gap-1 px-3 py-2 transition-all duration-200 flex-shrink-0 hidden sm:flex",
736
- title: "No controller detected - press any button on your gamepad to connect",
1039
+ className: "relative group flex-col items-center gap-1 px-3 py-2 transition-all duration-200 flex-shrink-0",
1040
+ title: t.controls.noGamepad,
737
1041
  style: {
738
1042
  border: "2px dashed #6b7280",
739
1043
  backgroundColor: "transparent"
740
1044
  },
741
1045
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center gap-2", children: [
742
1046
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { size: 18, className: "text-gray-400 transition-transform group-hover:scale-110 group-hover:text-white" }),
743
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-bold uppercase tracking-wider text-gray-400 group-hover:text-white whitespace-nowrap", children: "Press" }),
1047
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-bold uppercase tracking-wider text-gray-400 group-hover:text-white whitespace-nowrap", children: t.controls.press }),
744
1048
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-5 rounded-full border-2 border-gray-400 group-hover:border-white flex items-center justify-center animate-pulse", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 rounded-full bg-gray-400 group-hover:bg-white" }) })
745
1049
  ] })
746
1050
  }
@@ -752,12 +1056,18 @@ var SettingsControls = React5.memo(function SettingsControls2({
752
1056
  {
753
1057
  onClick: hardcoreRestrictions?.canUseCheats === false ? void 0 : onCheats,
754
1058
  icon: lucideReact.Code,
755
- label: "Cheats",
1059
+ label: t.settings.cheats,
756
1060
  disabled: disabled || hardcoreRestrictions?.canUseCheats === false,
757
1061
  systemColor
758
1062
  }
759
1063
  ),
760
- /* @__PURE__ */ jsxRuntime.jsx(HardcoreTooltip, { show: hardcoreRestrictions?.canUseCheats === false })
1064
+ /* @__PURE__ */ jsxRuntime.jsx(
1065
+ HardcoreTooltip,
1066
+ {
1067
+ show: hardcoreRestrictions?.canUseCheats === false,
1068
+ message: hardcoreRestrictions?.isHardcore ? t.common.disabledInHardcore : t.common.notSupported
1069
+ }
1070
+ )
761
1071
  ] }),
762
1072
  /* @__PURE__ */ jsxRuntime.jsx(
763
1073
  RAButton,
@@ -771,16 +1081,28 @@ var SettingsControls = React5.memo(function SettingsControls2({
771
1081
  className: "hidden sm:flex"
772
1082
  }
773
1083
  ),
1084
+ onSettings && /* @__PURE__ */ jsxRuntime.jsx(
1085
+ ControlButton,
1086
+ {
1087
+ onClick: onSettings,
1088
+ icon: lucideReact.Settings,
1089
+ label: t.settings.title,
1090
+ disabled,
1091
+ systemColor,
1092
+ className: "hidden sm:flex"
1093
+ }
1094
+ ),
774
1095
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-8 bg-white/10 mx-2 hidden sm:block" }),
775
- /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onExit, icon: lucideReact.Power, label: "Exit", danger: true, disabled, systemColor })
1096
+ /* @__PURE__ */ jsxRuntime.jsx(ControlButton, { onClick: onExit, icon: lucideReact.Power, label: t.settings.exit, danger: true, disabled, systemColor })
776
1097
  ] });
777
1098
  });
778
- var MobileControlDrawer = React5.memo(function MobileControlDrawer2({
1099
+ var MobileControlDrawer = React2.memo(function MobileControlDrawer2({
779
1100
  children,
780
1101
  systemColor = "#00FF41"
781
1102
  }) {
782
- const [isExpanded, setIsExpanded] = React5.useState(false);
783
- React5.useEffect(() => {
1103
+ const t = useKoinTranslation();
1104
+ const [isExpanded, setIsExpanded] = React2.useState(false);
1105
+ React2.useEffect(() => {
784
1106
  if (isExpanded) {
785
1107
  const handleInteract = (e) => {
786
1108
  const target = e.target;
@@ -813,7 +1135,7 @@ var MobileControlDrawer = React5.memo(function MobileControlDrawer2({
813
1135
  boxShadow: isExpanded ? `0 0 20px ${systemColor}60, 0 4px 20px rgba(0,0,0,0.5)` : "0 4px 20px rgba(0,0,0,0.5)",
814
1136
  borderColor: isExpanded ? systemColor : void 0
815
1137
  },
816
- "aria-label": isExpanded ? "Close menu" : "Open menu",
1138
+ "aria-label": isExpanded ? t.controls.menuClose : t.controls.menuOpen,
817
1139
  children: /* @__PURE__ */ jsxRuntime.jsxs(
818
1140
  "svg",
819
1141
  {
@@ -854,7 +1176,7 @@ var MobileControlDrawer = React5.memo(function MobileControlDrawer2({
854
1176
  /* @__PURE__ */ jsxRuntime.jsx(
855
1177
  "div",
856
1178
  {
857
- className: "relative flex flex-wrap items-center justify-center gap-3 px-4 py-6 pb-20",
1179
+ className: "relative flex flex-col items-center px-4 py-6 pb-20",
858
1180
  style: { paddingBottom: "calc(env(safe-area-inset-bottom, 20px) + 80px)" },
859
1181
  children
860
1182
  }
@@ -866,7 +1188,7 @@ var MobileControlDrawer = React5.memo(function MobileControlDrawer2({
866
1188
  ] });
867
1189
  });
868
1190
  var MobileControlDrawer_default = MobileControlDrawer;
869
- var PlayerControls = React5.memo(function PlayerControls2({
1191
+ var PlayerControls = React2.memo(function PlayerControls2({
870
1192
  isPaused,
871
1193
  isRunning,
872
1194
  speed,
@@ -902,6 +1224,7 @@ var PlayerControls = React5.memo(function PlayerControls2({
902
1224
  systemColor = "#00FF41",
903
1225
  gamepadCount = 0,
904
1226
  onGamepadSettings,
1227
+ onSettings,
905
1228
  volume = 100,
906
1229
  isMuted = false,
907
1230
  onVolumeChange,
@@ -934,6 +1257,7 @@ var PlayerControls = React5.memo(function PlayerControls2({
934
1257
  hardcoreRestrictions
935
1258
  }
936
1259
  ),
1260
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-px bg-white/10 sm:hidden my-4" }),
937
1261
  /* @__PURE__ */ jsxRuntime.jsx(
938
1262
  SaveLoadControls,
939
1263
  {
@@ -954,12 +1278,14 @@ var PlayerControls = React5.memo(function PlayerControls2({
954
1278
  onAutoSaveToggle
955
1279
  }
956
1280
  ),
1281
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-px bg-white/10 sm:hidden my-4" }),
957
1282
  /* @__PURE__ */ jsxRuntime.jsx(
958
1283
  SettingsControls,
959
1284
  {
960
1285
  onFullscreen,
961
1286
  onControls,
962
1287
  onGamepadSettings,
1288
+ onSettings,
963
1289
  onCheats,
964
1290
  onRetroAchievements,
965
1291
  onShowShortcuts,
@@ -980,7 +1306,7 @@ var PlayerControls = React5.memo(function PlayerControls2({
980
1306
  ] });
981
1307
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
982
1308
  /* @__PURE__ */ jsxRuntime.jsx(MobileControlDrawer_default, { systemColor, children: controlsContent }),
983
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:flex w-full items-center justify-between gap-2 px-4 py-2 bg-black backdrop-blur-sm border-t border-white/10 shrink-0 overflow-x-auto", children: controlsContent })
1309
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:flex w-full bg-black/90 backdrop-blur-md border-t border-white/10 shrink-0 z-50 overflow-x-auto no-scrollbar", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center min-w-max mx-auto gap-4 px-8 py-4", children: controlsContent }) })
984
1310
  ] });
985
1311
  });
986
1312
  var PlayerControls_default = PlayerControls;
@@ -1028,11 +1354,11 @@ var TOAST_CONFIGS = {
1028
1354
  }
1029
1355
  };
1030
1356
  function ToastItem({ toast, onDismiss }) {
1031
- const [isVisible, setIsVisible] = React5.useState(false);
1032
- const [isExiting, setIsExiting] = React5.useState(false);
1357
+ const [isVisible, setIsVisible] = React2.useState(false);
1358
+ const [isExiting, setIsExiting] = React2.useState(false);
1033
1359
  const config = TOAST_CONFIGS[toast.type];
1034
1360
  const IconComponent = config.icon;
1035
- React5.useEffect(() => {
1361
+ React2.useEffect(() => {
1036
1362
  requestAnimationFrame(() => {
1037
1363
  setIsVisible(true);
1038
1364
  });
@@ -1138,17 +1464,18 @@ function ToastContainer({ toasts, onDismiss }) {
1138
1464
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none", children: toasts.map((toast) => /* @__PURE__ */ jsxRuntime.jsx(ToastItem, { toast, onDismiss }, toast.id)) })
1139
1465
  ] });
1140
1466
  }
1141
- var PerformanceOverlay = React5.memo(function PerformanceOverlay2({
1467
+ var PerformanceOverlay = React2.memo(function PerformanceOverlay2({
1142
1468
  isVisible,
1143
1469
  coreName = "Unknown",
1144
1470
  systemColor = "#00FF41"
1145
1471
  }) {
1146
- const [fps, setFps] = React5.useState(0);
1147
- const [frameTime, setFrameTime] = React5.useState(0);
1148
- const frameTimesRef = React5.useRef([]);
1149
- const lastTimeRef = React5.useRef(performance.now());
1150
- const rafIdRef = React5.useRef(null);
1151
- React5.useEffect(() => {
1472
+ const t = useKoinTranslation();
1473
+ const [fps, setFps] = React2.useState(0);
1474
+ const [frameTime, setFrameTime] = React2.useState(0);
1475
+ const frameTimesRef = React2.useRef([]);
1476
+ const lastTimeRef = React2.useRef(performance.now());
1477
+ const rafIdRef = React2.useRef(null);
1478
+ React2.useEffect(() => {
1152
1479
  if (!isVisible) {
1153
1480
  if (rafIdRef.current) {
1154
1481
  cancelAnimationFrame(rafIdRef.current);
@@ -1194,12 +1521,12 @@ var PerformanceOverlay = React5.memo(function PerformanceOverlay2({
1194
1521
  },
1195
1522
  children: [
1196
1523
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1197
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60", children: "FPS" }),
1524
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60 text-[10px] uppercase", children: t.overlays.performance.fps }),
1198
1525
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold", children: fps })
1199
1526
  ] }),
1200
1527
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-3 bg-white/20" }),
1201
1528
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1202
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60", children: "Frame" }),
1529
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60 text-[10px] uppercase", children: t.overlays.performance.frameTime }),
1203
1530
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-bold", children: [
1204
1531
  frameTime,
1205
1532
  "ms"
@@ -1207,7 +1534,7 @@ var PerformanceOverlay = React5.memo(function PerformanceOverlay2({
1207
1534
  ] }),
1208
1535
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-3 bg-white/20" }),
1209
1536
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1210
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60", children: "Core" }),
1537
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60 text-[10px] uppercase", children: t.overlays.performance.core }),
1211
1538
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold text-white/80", children: coreName })
1212
1539
  ] })
1213
1540
  ]
@@ -1254,14 +1581,14 @@ var KEY_TO_BUTTON = {
1254
1581
  "ShiftRight": "SELECT",
1255
1582
  "Space": "COIN"
1256
1583
  };
1257
- var InputDisplay = React5.memo(function InputDisplay2({
1584
+ var InputDisplay = React2.memo(function InputDisplay2({
1258
1585
  isVisible,
1259
1586
  system,
1260
1587
  systemColor = "#00FF41",
1261
1588
  position = "bottom-right"
1262
1589
  }) {
1263
- const [activeKeys, setActiveKeys] = React5.useState(/* @__PURE__ */ new Set());
1264
- React5.useEffect(() => {
1590
+ const [activeKeys, setActiveKeys] = React2.useState(/* @__PURE__ */ new Set());
1591
+ React2.useEffect(() => {
1265
1592
  if (!isVisible) return;
1266
1593
  const handleKeyDown = (e) => {
1267
1594
  const button = KEY_TO_BUTTON[e.code];
@@ -1335,7 +1662,7 @@ var InputDisplay = React5.memo(function InputDisplay2({
1335
1662
  }
1336
1663
  );
1337
1664
  });
1338
- var DpadButton = React5.memo(function DpadButton2({
1665
+ var DpadButton = React2.memo(function DpadButton2({
1339
1666
  active,
1340
1667
  color,
1341
1668
  children
@@ -1353,7 +1680,7 @@ var DpadButton = React5.memo(function DpadButton2({
1353
1680
  }
1354
1681
  );
1355
1682
  });
1356
- var ActionButton = React5.memo(function ActionButton2({
1683
+ var ActionButton = React2.memo(function ActionButton2({
1357
1684
  active,
1358
1685
  color,
1359
1686
  children
@@ -1372,7 +1699,7 @@ var ActionButton = React5.memo(function ActionButton2({
1372
1699
  );
1373
1700
  });
1374
1701
  var InputDisplay_default = InputDisplay;
1375
- var RecordingIndicator = React5.memo(function RecordingIndicator2({
1702
+ var RecordingIndicator = React2.memo(function RecordingIndicator2({
1376
1703
  isRecording,
1377
1704
  isPaused,
1378
1705
  duration,
@@ -1381,7 +1708,8 @@ var RecordingIndicator = React5.memo(function RecordingIndicator2({
1381
1708
  onStop,
1382
1709
  systemColor = "#FF3333"
1383
1710
  }) {
1384
- const [isHovered, setIsHovered] = React5.useState(false);
1711
+ const t = useKoinTranslation();
1712
+ const [isHovered, setIsHovered] = React2.useState(false);
1385
1713
  if (!isRecording) return null;
1386
1714
  const minutes = Math.floor(duration / 60);
1387
1715
  const seconds = duration % 60;
@@ -1428,7 +1756,7 @@ var RecordingIndicator = React5.memo(function RecordingIndicator2({
1428
1756
  {
1429
1757
  className: "text-xs font-bold",
1430
1758
  style: { color: isPaused ? "#FFA500" : "#FF3333" },
1431
- children: isPaused ? "PAUSED" : "RECORDING"
1759
+ children: isPaused ? t.overlays.recording.paused : t.overlays.recording.recording
1432
1760
  }
1433
1761
  ),
1434
1762
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-white/60", children: timeString })
@@ -1439,7 +1767,7 @@ var RecordingIndicator = React5.memo(function RecordingIndicator2({
1439
1767
  {
1440
1768
  onClick: onResume,
1441
1769
  className: "p-1.5 rounded hover:bg-white/20 transition-colors",
1442
- title: "Resume recording",
1770
+ title: t.overlays.recording.resume,
1443
1771
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { size: 14, className: "text-orange-400", fill: "#FFA500" })
1444
1772
  }
1445
1773
  ) : /* @__PURE__ */ jsxRuntime.jsx(
@@ -1447,7 +1775,7 @@ var RecordingIndicator = React5.memo(function RecordingIndicator2({
1447
1775
  {
1448
1776
  onClick: onPause,
1449
1777
  className: "p-1.5 rounded hover:bg-white/20 transition-colors",
1450
- title: "Pause recording",
1778
+ title: t.overlays.recording.pause,
1451
1779
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pause, { size: 14, className: "text-white/80" })
1452
1780
  }
1453
1781
  ),
@@ -1456,7 +1784,7 @@ var RecordingIndicator = React5.memo(function RecordingIndicator2({
1456
1784
  {
1457
1785
  onClick: onStop,
1458
1786
  className: "p-1.5 rounded hover:bg-red-500/30 transition-colors flex items-center gap-1",
1459
- title: "Stop and save recording",
1787
+ title: t.overlays.recording.stop,
1460
1788
  children: [
1461
1789
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { size: 12, fill: "#FF3333", className: "text-red-500" }),
1462
1790
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 12, className: "text-white/60" })
@@ -1467,39 +1795,40 @@ var RecordingIndicator = React5.memo(function RecordingIndicator2({
1467
1795
  ]
1468
1796
  }
1469
1797
  ),
1470
- !isHovered && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[9px] text-white/40 text-center mt-1", children: "Hover for controls" })
1798
+ !isHovered && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[9px] text-white/40 text-center mt-1", children: t.overlays.recording.hover })
1471
1799
  ]
1472
1800
  }
1473
1801
  );
1474
1802
  });
1475
1803
  var RecordingIndicator_default = RecordingIndicator;
1476
- var SHORTCUTS = [
1477
- {
1478
- section: "Overlays",
1479
- items: [
1480
- { key: "F1", description: "Show this help" },
1481
- { key: "F3", description: "Performance Overlay (FPS)" },
1482
- { key: "F4", description: "Input Display" }
1483
- ]
1484
- },
1485
- {
1486
- section: "Recording",
1487
- items: [
1488
- { key: "F5", description: "Start/Stop Recording" }
1489
- ]
1490
- },
1491
- {
1492
- section: "Audio",
1493
- items: [
1494
- { key: "F9", description: "Toggle Mute" }
1495
- ]
1496
- }
1497
- ];
1498
- var ShortcutsModal = React5.memo(function ShortcutsModal2({
1804
+ var ShortcutsModal = React2.memo(function ShortcutsModal2({
1499
1805
  isOpen,
1500
1806
  onClose,
1501
1807
  systemColor = "#00FF41"
1502
1808
  }) {
1809
+ const t = useKoinTranslation();
1810
+ const shortcuts = React2.useMemo(() => [
1811
+ {
1812
+ section: t.modals.shortcuts.overlays,
1813
+ items: [
1814
+ { key: "F1", description: t.modals.shortcuts.showHelp },
1815
+ { key: "F3", description: t.modals.shortcuts.perfOverlay },
1816
+ { key: "F4", description: t.modals.shortcuts.inputDisplay }
1817
+ ]
1818
+ },
1819
+ {
1820
+ section: t.modals.shortcuts.recording,
1821
+ items: [
1822
+ { key: "F5", description: t.modals.shortcuts.toggleRec }
1823
+ ]
1824
+ },
1825
+ {
1826
+ section: t.settings.audio,
1827
+ items: [
1828
+ { key: "F9", description: t.modals.shortcuts.toggleMute }
1829
+ ]
1830
+ }
1831
+ ], [t]);
1503
1832
  if (!isOpen) return null;
1504
1833
  return /* @__PURE__ */ jsxRuntime.jsx(
1505
1834
  "div",
@@ -1521,7 +1850,7 @@ var ShortcutsModal = React5.memo(function ShortcutsModal2({
1521
1850
  children: [
1522
1851
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1523
1852
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Keyboard, { size: 18, style: { color: systemColor } }),
1524
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold text-white", children: "Player Shortcuts" })
1853
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold text-white", children: t.modals.shortcuts.playerShortcuts })
1525
1854
  ] }),
1526
1855
  /* @__PURE__ */ jsxRuntime.jsx(
1527
1856
  "button",
@@ -1535,7 +1864,7 @@ var ShortcutsModal = React5.memo(function ShortcutsModal2({
1535
1864
  }
1536
1865
  ),
1537
1866
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-3", children: [
1538
- SHORTCUTS.map(({ section, items }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1867
+ shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1539
1868
  /* @__PURE__ */ jsxRuntime.jsx(
1540
1869
  "h3",
1541
1870
  {
@@ -1569,20 +1898,16 @@ var ShortcutsModal = React5.memo(function ShortcutsModal2({
1569
1898
  ] }, section)),
1570
1899
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
1571
1900
  "Game controls can be configured in ",
1572
- /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white/60", children: "Keys" }),
1901
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white/60", children: t.controls.keys }),
1573
1902
  " settings."
1574
1903
  ] })
1575
1904
  ] }),
1576
- /* @__PURE__ */ jsxRuntime.jsxs(
1905
+ /* @__PURE__ */ jsxRuntime.jsx(
1577
1906
  "div",
1578
1907
  {
1579
1908
  className: "px-4 py-2 text-center text-xs text-white/40 border-t",
1580
1909
  style: { borderColor: `${systemColor}20` },
1581
- children: [
1582
- "Press ",
1583
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "px-1 bg-white/10 rounded font-mono", children: "ESC" }),
1584
- " to close"
1585
- ]
1910
+ children: t.modals.shortcuts.pressEsc
1586
1911
  }
1587
1912
  )
1588
1913
  ]
@@ -1705,10 +2030,10 @@ function createOrientationChangeHandler(callback, checkReady, maxRafs = 3) {
1705
2030
 
1706
2031
  // src/hooks/useMobile.ts
1707
2032
  function useMobile() {
1708
- const [isMobile, setIsMobile] = React5.useState(false);
1709
- const [isLandscape, setIsLandscape] = React5.useState(false);
1710
- const [isPortrait, setIsPortrait] = React5.useState(true);
1711
- React5.useEffect(() => {
2033
+ const [isMobile, setIsMobile] = React2.useState(false);
2034
+ const [isLandscape, setIsLandscape] = React2.useState(false);
2035
+ const [isPortrait, setIsPortrait] = React2.useState(true);
2036
+ React2.useEffect(() => {
1712
2037
  let lastOrientation = null;
1713
2038
  let orientationTimeout = null;
1714
2039
  const checkOrientation = () => {
@@ -1860,17 +2185,17 @@ function useTouchHandlers({
1860
2185
  onRelease,
1861
2186
  onPositionChange
1862
2187
  }) {
1863
- const isDraggingRef = React5.useRef(false);
1864
- const dragStartRef = React5.useRef({ x: 0, y: 0 });
1865
- const dragTimerRef = React5.useRef(null);
1866
- const touchStartPosRef = React5.useRef({ x: 0, y: 0 });
1867
- const clearDragTimer = React5.useCallback(() => {
2188
+ const isDraggingRef = React2.useRef(false);
2189
+ const dragStartRef = React2.useRef({ x: 0, y: 0 });
2190
+ const dragTimerRef = React2.useRef(null);
2191
+ const touchStartPosRef = React2.useRef({ x: 0, y: 0 });
2192
+ const clearDragTimer = React2.useCallback(() => {
1868
2193
  if (dragTimerRef.current) {
1869
2194
  clearTimeout(dragTimerRef.current);
1870
2195
  dragTimerRef.current = null;
1871
2196
  }
1872
2197
  }, []);
1873
- const startDragging = React5.useCallback(
2198
+ const startDragging = React2.useCallback(
1874
2199
  (touchX, touchY) => {
1875
2200
  isDraggingRef.current = true;
1876
2201
  dragStartRef.current = {
@@ -1883,7 +2208,7 @@ function useTouchHandlers({
1883
2208
  },
1884
2209
  [displayX, displayY, containerWidth, containerHeight, isSystemButton, buttonType, onRelease]
1885
2210
  );
1886
- const handleTouchStart = React5.useCallback(
2211
+ const handleTouchStart = React2.useCallback(
1887
2212
  (e) => {
1888
2213
  const touch = e.touches[0];
1889
2214
  touchStartPosRef.current = { x: touch.clientX, y: touch.clientY };
@@ -1918,7 +2243,7 @@ function useTouchHandlers({
1918
2243
  },
1919
2244
  [isSystemButton, buttonType, onPress, onPressDown, onPositionChange, buttonSize, startDragging]
1920
2245
  );
1921
- const handleTouchMove = React5.useCallback(
2246
+ const handleTouchMove = React2.useCallback(
1922
2247
  (e) => {
1923
2248
  const touch = e.touches[0];
1924
2249
  if (onPositionChange && !isDraggingRef.current) {
@@ -1945,7 +2270,7 @@ function useTouchHandlers({
1945
2270
  },
1946
2271
  [onPositionChange, clearDragTimer, startDragging, containerWidth, containerHeight, buttonSize]
1947
2272
  );
1948
- const handleTouchEnd = React5.useCallback(
2273
+ const handleTouchEnd = React2.useCallback(
1949
2274
  (e) => {
1950
2275
  clearDragTimer();
1951
2276
  if (isDraggingRef.current) {
@@ -1962,7 +2287,7 @@ function useTouchHandlers({
1962
2287
  },
1963
2288
  [clearDragTimer, isSystemButton, buttonType, onRelease]
1964
2289
  );
1965
- const handleTouchCancel = React5.useCallback(
2290
+ const handleTouchCancel = React2.useCallback(
1966
2291
  (e) => {
1967
2292
  clearDragTimer();
1968
2293
  if (isDraggingRef.current) {
@@ -1979,7 +2304,7 @@ function useTouchHandlers({
1979
2304
  },
1980
2305
  [clearDragTimer, isSystemButton, buttonType, onRelease]
1981
2306
  );
1982
- const cleanup = React5.useCallback(() => {
2307
+ const cleanup = React2.useCallback(() => {
1983
2308
  clearDragTimer();
1984
2309
  }, [clearDragTimer]);
1985
2310
  return {
@@ -2043,7 +2368,7 @@ function getButtonStyles(buttonType, isPressed) {
2043
2368
  };
2044
2369
  }
2045
2370
  }
2046
- var VirtualButton = React5__default.default.memo(function VirtualButton2({
2371
+ var VirtualButton = React2__default.default.memo(function VirtualButton2({
2047
2372
  config,
2048
2373
  isPressed,
2049
2374
  onPress,
@@ -2057,10 +2382,14 @@ var VirtualButton = React5__default.default.memo(function VirtualButton2({
2057
2382
  systemColor = "#00FF41"
2058
2383
  // Default retro green
2059
2384
  }) {
2060
- const buttonRef = React5.useRef(null);
2385
+ const t = useKoinTranslation();
2386
+ const buttonRef = React2.useRef(null);
2061
2387
  const isSystemButton = config.type === "start" || config.type === "select";
2062
2388
  const displayX = customPosition ? customPosition.x : config.x;
2063
2389
  const displayY = customPosition ? customPosition.y : config.y;
2390
+ let label = config.label;
2391
+ if (config.type === "start") label = t.controls.startBtn;
2392
+ if (config.type === "select") label = t.controls.selectBtn;
2064
2393
  const {
2065
2394
  handleTouchStart,
2066
2395
  handleTouchMove,
@@ -2080,7 +2409,7 @@ var VirtualButton = React5__default.default.memo(function VirtualButton2({
2080
2409
  onRelease,
2081
2410
  onPositionChange
2082
2411
  });
2083
- React5.useEffect(() => {
2412
+ React2.useEffect(() => {
2084
2413
  const button = buttonRef.current;
2085
2414
  if (!button) return;
2086
2415
  button.addEventListener("touchstart", handleTouchStart, { passive: false });
@@ -2138,9 +2467,9 @@ var VirtualButton = React5__default.default.memo(function VirtualButton2({
2138
2467
  userSelect: "none",
2139
2468
  ...pressedStyle
2140
2469
  },
2141
- "aria-label": config.label,
2470
+ "aria-label": label,
2142
2471
  onContextMenu: (e) => e.preventDefault(),
2143
- children: config.label
2472
+ children: label
2144
2473
  }
2145
2474
  );
2146
2475
  });
@@ -2757,7 +3086,7 @@ function dispatchKeyboardEvent(type, code) {
2757
3086
  canvas.dispatchEvent(event);
2758
3087
  return true;
2759
3088
  }
2760
- var Dpad = React5__default.default.memo(function Dpad2({
3089
+ var Dpad = React2__default.default.memo(function Dpad2({
2761
3090
  size,
2762
3091
  x,
2763
3092
  y,
@@ -2767,14 +3096,14 @@ var Dpad = React5__default.default.memo(function Dpad2({
2767
3096
  systemColor = "#00FF41",
2768
3097
  isLandscape = false
2769
3098
  }) {
2770
- const dpadRef = React5.useRef(null);
2771
- const activeTouchRef = React5.useRef(null);
2772
- const activeDirectionsRef = React5.useRef(/* @__PURE__ */ new Set());
2773
- const upRef = React5.useRef(null);
2774
- const downRef = React5.useRef(null);
2775
- const leftRef = React5.useRef(null);
2776
- const rightRef = React5.useRef(null);
2777
- const getKeyCode = React5.useCallback((direction) => {
3099
+ const dpadRef = React2.useRef(null);
3100
+ const activeTouchRef = React2.useRef(null);
3101
+ const activeDirectionsRef = React2.useRef(/* @__PURE__ */ new Set());
3102
+ const upRef = React2.useRef(null);
3103
+ const downRef = React2.useRef(null);
3104
+ const leftRef = React2.useRef(null);
3105
+ const rightRef = React2.useRef(null);
3106
+ const getKeyCode = React2.useCallback((direction) => {
2778
3107
  if (!controls) {
2779
3108
  const defaults = {
2780
3109
  up: "ArrowUp",
@@ -2786,7 +3115,7 @@ var Dpad = React5__default.default.memo(function Dpad2({
2786
3115
  }
2787
3116
  return controls[direction] || "";
2788
3117
  }, [controls]);
2789
- const getDirectionsFromTouch = React5.useCallback((touchX, touchY, rect) => {
3118
+ const getDirectionsFromTouch = React2.useCallback((touchX, touchY, rect) => {
2790
3119
  const centerX = rect.left + rect.width / 2;
2791
3120
  const centerY = rect.top + rect.height / 2;
2792
3121
  const dx = touchX - centerX;
@@ -2802,7 +3131,7 @@ var Dpad = React5__default.default.memo(function Dpad2({
2802
3131
  if (angle >= -67.5 && angle <= 67.5) directions.add("right");
2803
3132
  return directions;
2804
3133
  }, []);
2805
- const updateVisuals = React5.useCallback((directions) => {
3134
+ const updateVisuals = React2.useCallback((directions) => {
2806
3135
  const activeColor = systemColor;
2807
3136
  const inactiveColor = "#1a1a1a";
2808
3137
  if (upRef.current) upRef.current.style.fill = directions.has("up") ? activeColor : inactiveColor;
@@ -2810,7 +3139,7 @@ var Dpad = React5__default.default.memo(function Dpad2({
2810
3139
  if (leftRef.current) leftRef.current.style.fill = directions.has("left") ? activeColor : inactiveColor;
2811
3140
  if (rightRef.current) rightRef.current.style.fill = directions.has("right") ? activeColor : inactiveColor;
2812
3141
  }, [systemColor]);
2813
- const updateDirections = React5.useCallback((newDirections) => {
3142
+ const updateDirections = React2.useCallback((newDirections) => {
2814
3143
  const prev = activeDirectionsRef.current;
2815
3144
  prev.forEach((dir) => {
2816
3145
  if (!newDirections.has(dir)) {
@@ -2830,7 +3159,7 @@ var Dpad = React5__default.default.memo(function Dpad2({
2830
3159
  activeDirectionsRef.current = newDirections;
2831
3160
  updateVisuals(newDirections);
2832
3161
  }, [getKeyCode, updateVisuals]);
2833
- const handleTouchStart = React5.useCallback((e) => {
3162
+ const handleTouchStart = React2.useCallback((e) => {
2834
3163
  e.preventDefault();
2835
3164
  e.stopPropagation();
2836
3165
  const touch = e.touches[0];
@@ -2839,7 +3168,7 @@ var Dpad = React5__default.default.memo(function Dpad2({
2839
3168
  if (!rect) return;
2840
3169
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
2841
3170
  }, [getDirectionsFromTouch, updateDirections]);
2842
- const handleTouchMove = React5.useCallback((e) => {
3171
+ const handleTouchMove = React2.useCallback((e) => {
2843
3172
  e.preventDefault();
2844
3173
  let touch = null;
2845
3174
  for (let i = 0; i < e.touches.length; i++) {
@@ -2853,7 +3182,7 @@ var Dpad = React5__default.default.memo(function Dpad2({
2853
3182
  if (!rect) return;
2854
3183
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
2855
3184
  }, [getDirectionsFromTouch, updateDirections]);
2856
- const handleTouchEnd = React5.useCallback((e) => {
3185
+ const handleTouchEnd = React2.useCallback((e) => {
2857
3186
  e.preventDefault();
2858
3187
  let touchEnded = true;
2859
3188
  for (let i = 0; i < e.touches.length; i++) {
@@ -2872,7 +3201,7 @@ var Dpad = React5__default.default.memo(function Dpad2({
2872
3201
  updateVisuals(/* @__PURE__ */ new Set());
2873
3202
  }
2874
3203
  }, [getKeyCode, updateVisuals]);
2875
- React5.useEffect(() => {
3204
+ React2.useEffect(() => {
2876
3205
  const dpad = dpadRef.current;
2877
3206
  if (!dpad) return;
2878
3207
  dpad.addEventListener("touchstart", handleTouchStart, { passive: false });
@@ -2930,9 +3259,9 @@ function adjustButtonPosition(config, context) {
2930
3259
  }
2931
3260
  var STORAGE_KEY = "virtual-button-positions";
2932
3261
  function useButtonPositions() {
2933
- const [landscapePositions, setLandscapePositions] = React5.useState({});
2934
- const [portraitPositions, setPortraitPositions] = React5.useState({});
2935
- React5.useEffect(() => {
3262
+ const [landscapePositions, setLandscapePositions] = React2.useState({});
3263
+ const [portraitPositions, setPortraitPositions] = React2.useState({});
3264
+ React2.useEffect(() => {
2936
3265
  try {
2937
3266
  const stored = localStorage.getItem(STORAGE_KEY);
2938
3267
  if (stored) {
@@ -2948,7 +3277,7 @@ function useButtonPositions() {
2948
3277
  console.error("Failed to load button positions:", e);
2949
3278
  }
2950
3279
  }, []);
2951
- const savePosition = React5.useCallback((buttonType, x, y, isLandscape) => {
3280
+ const savePosition = React2.useCallback((buttonType, x, y, isLandscape) => {
2952
3281
  if (isLandscape) {
2953
3282
  setLandscapePositions((prev) => {
2954
3283
  const updated = { ...prev, [buttonType]: { x, y } };
@@ -2979,11 +3308,11 @@ function useButtonPositions() {
2979
3308
  });
2980
3309
  }
2981
3310
  }, [landscapePositions, portraitPositions]);
2982
- const getPosition = React5.useCallback((buttonType, isLandscape) => {
3311
+ const getPosition = React2.useCallback((buttonType, isLandscape) => {
2983
3312
  const positions = isLandscape ? landscapePositions : portraitPositions;
2984
3313
  return positions[buttonType] || null;
2985
3314
  }, [landscapePositions, portraitPositions]);
2986
- const resetPositions = React5.useCallback(() => {
3315
+ const resetPositions = React2.useCallback(() => {
2987
3316
  setLandscapePositions({});
2988
3317
  setPortraitPositions({});
2989
3318
  try {
@@ -3008,9 +3337,9 @@ function VirtualController({
3008
3337
  // Default retro green
3009
3338
  }) {
3010
3339
  const { isMobile, isLandscape, isPortrait } = useMobile();
3011
- const [pressedButtons, setPressedButtons] = React5.useState(/* @__PURE__ */ new Set());
3012
- const [containerSize, setContainerSize] = React5.useState({ width: 0, height: 0 });
3013
- const [isFullscreenState, setIsFullscreenState] = React5.useState(false);
3340
+ const [pressedButtons, setPressedButtons] = React2.useState(/* @__PURE__ */ new Set());
3341
+ const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
3342
+ const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
3014
3343
  const { getPosition, savePosition } = useButtonPositions();
3015
3344
  const layout = getLayoutForSystem(system);
3016
3345
  const visibleButtons = layout.buttons.filter((btn) => {
@@ -3021,7 +3350,7 @@ function VirtualController({
3021
3350
  });
3022
3351
  const DPAD_TYPES = ["up", "down", "left", "right"];
3023
3352
  const dpadButtons = visibleButtons.filter((btn) => DPAD_TYPES.includes(btn.type));
3024
- React5.useEffect(() => {
3353
+ React2.useEffect(() => {
3025
3354
  const updateSize = () => {
3026
3355
  const { width, height } = getViewportSize();
3027
3356
  setContainerSize({ width, height });
@@ -3064,14 +3393,14 @@ function VirtualController({
3064
3393
  cleanupFullscreen();
3065
3394
  };
3066
3395
  }, [containerSize.height]);
3067
- const getButtonKeyboardCode = React5.useCallback(
3396
+ const getButtonKeyboardCode = React2.useCallback(
3068
3397
  (buttonType) => {
3069
3398
  return getKeyboardCode(buttonType, controls);
3070
3399
  },
3071
3400
  [controls]
3072
3401
  );
3073
3402
  const SYSTEM_BUTTONS2 = ["start", "select"];
3074
- const handlePress = React5.useCallback(
3403
+ const handlePress = React2.useCallback(
3075
3404
  (buttonType) => {
3076
3405
  const isSystemButton = SYSTEM_BUTTONS2.includes(buttonType);
3077
3406
  if (!isSystemButton && !isRunning) {
@@ -3094,7 +3423,7 @@ function VirtualController({
3094
3423
  },
3095
3424
  [isRunning, getButtonKeyboardCode]
3096
3425
  );
3097
- const handlePressDown = React5.useCallback(
3426
+ const handlePressDown = React2.useCallback(
3098
3427
  (buttonType) => {
3099
3428
  if (!isRunning) return;
3100
3429
  const isSystemButton = SYSTEM_BUTTONS2.includes(buttonType);
@@ -3111,7 +3440,7 @@ function VirtualController({
3111
3440
  },
3112
3441
  [isRunning, getButtonKeyboardCode]
3113
3442
  );
3114
- const handleRelease = React5.useCallback(
3443
+ const handleRelease = React2.useCallback(
3115
3444
  (buttonType) => {
3116
3445
  const isSystemButton = SYSTEM_BUTTONS2.includes(buttonType);
3117
3446
  if (isSystemButton) return;
@@ -3127,7 +3456,7 @@ function VirtualController({
3127
3456
  },
3128
3457
  [getButtonKeyboardCode]
3129
3458
  );
3130
- React5.useEffect(() => {
3459
+ React2.useEffect(() => {
3131
3460
  if (!isRunning && pressedButtons.size > 0) {
3132
3461
  pressedButtons.forEach((buttonType) => {
3133
3462
  if (!SYSTEM_BUTTONS2.includes(buttonType)) {
@@ -3137,7 +3466,7 @@ function VirtualController({
3137
3466
  setPressedButtons(/* @__PURE__ */ new Set());
3138
3467
  }
3139
3468
  }, [isRunning, pressedButtons, handleRelease]);
3140
- const memoizedButtonElements = React5.useMemo(() => {
3469
+ const memoizedButtonElements = React2.useMemo(() => {
3141
3470
  const width = containerSize.width || (typeof window !== "undefined" ? window.innerWidth : 0);
3142
3471
  const height = containerSize.height || (typeof window !== "undefined" ? window.innerHeight : 0);
3143
3472
  const context = {
@@ -3269,9 +3598,10 @@ function GameOverlay({
3269
3598
  isLoadingSave,
3270
3599
  onSelectBios
3271
3600
  }) {
3601
+ const t = useKoinTranslation();
3272
3602
  const isLoading = status === "loading" || status === "ready" && isLoadingSave;
3273
3603
  if (isLoading) {
3274
- const message = status === "loading" ? { title: `Loading ${system}`, subtitle: "Initializing emulator" } : { title: "Loading Save", subtitle: `Preparing slot ${pendingSlot}` };
3604
+ const message = status === "loading" ? { title: t.overlay.loading.replace("{{system}}", system || ""), subtitle: t.overlay.initializing } : { title: t.overlay.loadingSave, subtitle: t.overlay.preparingSlot.replace("{{num}}", String(pendingSlot || "")) };
3275
3605
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-black z-20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-4", children: [
3276
3606
  /* @__PURE__ */ jsxRuntime.jsx(LoadingSpinner, { color: systemColor, size: "lg" }),
3277
3607
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
@@ -3309,12 +3639,11 @@ function GameOverlay({
3309
3639
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { className: "w-8 h-8 ml-1", style: { color: systemColor }, fill: systemColor })
3310
3640
  }
3311
3641
  ),
3312
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-lg uppercase tracking-wider", style: { color: systemColor }, children: hasPendingSave ? "Continue" : "Play" }),
3642
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-lg uppercase tracking-wider", style: { color: systemColor }, children: t.overlay.play }),
3313
3643
  hasPendingSave && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-400 text-xs flex items-center gap-1", children: [
3314
3644
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 12 }),
3315
- " Slot ",
3316
- pendingSlot,
3317
- " ready"
3645
+ " ",
3646
+ t.overlay.slotReady.replace("{{num}}", String(pendingSlot))
3318
3647
  ] })
3319
3648
  ]
3320
3649
  }
@@ -3326,7 +3655,7 @@ function GameOverlay({
3326
3655
  className: "mt-6 flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors",
3327
3656
  children: [
3328
3657
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 rounded-full", style: { backgroundColor: systemColor } }),
3329
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono uppercase tracking-wider text-xs", children: "System Firmware" })
3658
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono uppercase tracking-wider text-xs", children: t.overlay.systemFirmware })
3330
3659
  ]
3331
3660
  }
3332
3661
  )
@@ -3336,8 +3665,8 @@ function GameOverlay({
3336
3665
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-black/90 z-20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-4", children: [
3337
3666
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "w-12 h-12 text-red-500" }),
3338
3667
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
3339
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-red-400 font-mono uppercase tracking-widest text-sm", children: "System Error" }),
3340
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-xs mt-1 max-w-xs", children: error || "Failed to initialize emulator" })
3668
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-red-400 font-mono uppercase tracking-widest text-sm", children: t.overlay.systemError }),
3669
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-xs mt-1 max-w-xs", children: error || t.overlay.failedInit })
3341
3670
  ] }),
3342
3671
  /* @__PURE__ */ jsxRuntime.jsx(
3343
3672
  "button",
@@ -3345,7 +3674,7 @@ function GameOverlay({
3345
3674
  onClick: onStart,
3346
3675
  className: "mt-2 px-4 py-2 text-sm font-bold rounded-lg transition-colors",
3347
3676
  style: { backgroundColor: systemColor, color: "#000" },
3348
- children: "Retry"
3677
+ children: t.overlay.retry
3349
3678
  }
3350
3679
  )
3351
3680
  ] }) });
@@ -3363,7 +3692,7 @@ function GameOverlay({
3363
3692
  ] })
3364
3693
  }
3365
3694
  ),
3366
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-mono uppercase tracking-wider text-sm", style: { color: systemColor }, children: "Paused" })
3695
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-mono uppercase tracking-wider text-sm", style: { color: systemColor }, children: t.overlay.paused })
3367
3696
  ] }) });
3368
3697
  }
3369
3698
  return null;
@@ -3408,7 +3737,7 @@ function saveMuteState(muted) {
3408
3737
  if (typeof window === "undefined") return;
3409
3738
  localStorage.setItem(MUTE_KEY, muted.toString());
3410
3739
  }
3411
- var GameCanvas = React5.memo(function GameCanvas2({
3740
+ var GameCanvas = React2.memo(function GameCanvas2({
3412
3741
  status,
3413
3742
  system,
3414
3743
  error,
@@ -3419,8 +3748,8 @@ var GameCanvas = React5.memo(function GameCanvas2({
3419
3748
  canvasRef,
3420
3749
  onSelectBios
3421
3750
  }) {
3422
- const canvasContainerRef = React5.useRef(null);
3423
- React5.useEffect(() => {
3751
+ const canvasContainerRef = React2.useRef(null);
3752
+ React2.useEffect(() => {
3424
3753
  const container = canvasContainerRef.current;
3425
3754
  if (!container || canvasRef.current) return;
3426
3755
  const canvas = document.createElement("canvas");
@@ -3438,7 +3767,7 @@ var GameCanvas = React5.memo(function GameCanvas2({
3438
3767
  canvasRef.current = null;
3439
3768
  };
3440
3769
  }, [canvasRef]);
3441
- React5.useEffect(() => {
3770
+ React2.useEffect(() => {
3442
3771
  if (status !== "ready" && status !== "running" && status !== "paused") return;
3443
3772
  return setupCanvasResize(canvasContainerRef);
3444
3773
  }, [status]);
@@ -3472,23 +3801,24 @@ function ControlMapper({
3472
3801
  onClose,
3473
3802
  system
3474
3803
  }) {
3475
- const [localControls, setLocalControls] = React5.useState(controls);
3476
- const [listeningFor, setListeningFor] = React5.useState(null);
3477
- const activeButtons = React5.useMemo(() => {
3804
+ const t = useKoinTranslation();
3805
+ const [localControls, setLocalControls] = React2.useState(controls);
3806
+ const [listeningFor, setListeningFor] = React2.useState(null);
3807
+ const activeButtons = React2.useMemo(() => {
3478
3808
  return getConsoleButtons(system || "SNES");
3479
3809
  }, [system]);
3480
- const controlGroups = React5.useMemo(() => {
3810
+ const controlGroups = React2.useMemo(() => {
3481
3811
  return getFilteredGroups(activeButtons);
3482
3812
  }, [activeButtons]);
3483
- const defaultControls = React5.useMemo(() => {
3813
+ const defaultControls = React2.useMemo(() => {
3484
3814
  return getConsoleKeyboardDefaults(system || "SNES");
3485
3815
  }, [system]);
3486
- React5.useEffect(() => {
3816
+ React2.useEffect(() => {
3487
3817
  if (isOpen) {
3488
3818
  setLocalControls(controls);
3489
3819
  }
3490
3820
  }, [isOpen, controls]);
3491
- React5.useEffect(() => {
3821
+ React2.useEffect(() => {
3492
3822
  if (!isOpen) {
3493
3823
  setListeningFor(null);
3494
3824
  return;
@@ -3531,8 +3861,8 @@ function ControlMapper({
3531
3861
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
3532
3862
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { className: "text-retro-primary", size: 24 }),
3533
3863
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3534
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: "Control Mapping" }),
3535
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: "Click a button and press a key to remap" })
3864
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.controls.title }),
3865
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.controls.description })
3536
3866
  ] })
3537
3867
  ] }),
3538
3868
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3563,7 +3893,7 @@ function ControlMapper({
3563
3893
  px-2 py-1 rounded text-xs font-mono
3564
3894
  ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
3565
3895
  `,
3566
- children: listeningFor === btn ? "Press..." : formatKeyCode(localControls[btn] || "")
3896
+ children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
3567
3897
  }
3568
3898
  )
3569
3899
  ]
@@ -3579,7 +3909,7 @@ function ControlMapper({
3579
3909
  className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors",
3580
3910
  children: [
3581
3911
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
3582
- "Reset to Default"
3912
+ t.modals.controls.reset
3583
3913
  ]
3584
3914
  }
3585
3915
  ),
@@ -3590,7 +3920,7 @@ function ControlMapper({
3590
3920
  className: "flex items-center gap-2 px-6 py-2 rounded-lg bg-retro-primary text-black font-bold text-sm hover:bg-retro-primary/90 transition-colors",
3591
3921
  children: [
3592
3922
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
3593
- "Save Controls"
3923
+ t.modals.controls.save
3594
3924
  ]
3595
3925
  }
3596
3926
  )
@@ -3638,17 +3968,17 @@ function toGamepadInfo(gamepad) {
3638
3968
  }
3639
3969
  function useGamepad(options) {
3640
3970
  const { onConnect, onDisconnect } = options || {};
3641
- const [gamepads, setGamepads] = React5.useState([]);
3642
- const rafRef = React5.useRef(null);
3643
- const lastStateRef = React5.useRef("");
3644
- const prevCountRef = React5.useRef(0);
3645
- const onConnectRef = React5.useRef(onConnect);
3646
- const onDisconnectRef = React5.useRef(onDisconnect);
3647
- React5.useEffect(() => {
3971
+ const [gamepads, setGamepads] = React2.useState([]);
3972
+ const rafRef = React2.useRef(null);
3973
+ const lastStateRef = React2.useRef("");
3974
+ const prevCountRef = React2.useRef(0);
3975
+ const onConnectRef = React2.useRef(onConnect);
3976
+ const onDisconnectRef = React2.useRef(onDisconnect);
3977
+ React2.useEffect(() => {
3648
3978
  onConnectRef.current = onConnect;
3649
3979
  onDisconnectRef.current = onDisconnect;
3650
3980
  }, [onConnect, onDisconnect]);
3651
- const getGamepads = React5.useCallback(() => {
3981
+ const getGamepads = React2.useCallback(() => {
3652
3982
  if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
3653
3983
  return [];
3654
3984
  }
@@ -3662,14 +3992,14 @@ function useGamepad(options) {
3662
3992
  }
3663
3993
  return connected;
3664
3994
  }, []);
3665
- const getRawGamepad = React5.useCallback((index) => {
3995
+ const getRawGamepad = React2.useCallback((index) => {
3666
3996
  const rawGamepads = navigator.getGamepads?.() ?? [];
3667
3997
  return rawGamepads[index] ?? null;
3668
3998
  }, []);
3669
- const refresh = React5.useCallback(() => {
3999
+ const refresh = React2.useCallback(() => {
3670
4000
  setGamepads(getGamepads());
3671
4001
  }, [getGamepads]);
3672
- React5.useEffect(() => {
4002
+ React2.useEffect(() => {
3673
4003
  if (typeof window === "undefined" || typeof navigator === "undefined") {
3674
4004
  return;
3675
4005
  }
@@ -3770,11 +4100,12 @@ function GamepadMapper({
3770
4100
  onSave,
3771
4101
  systemColor = "#00FF41"
3772
4102
  }) {
3773
- const [selectedPlayer, setSelectedPlayer] = React5.useState(1);
3774
- const [bindings, setBindings] = React5.useState({});
3775
- const [listeningFor, setListeningFor] = React5.useState(null);
3776
- const rafRef = React5.useRef(null);
3777
- React5.useEffect(() => {
4103
+ const t = useKoinTranslation();
4104
+ const [selectedPlayer, setSelectedPlayer] = React2.useState(1);
4105
+ const [bindings, setBindings] = React2.useState({});
4106
+ const [listeningFor, setListeningFor] = React2.useState(null);
4107
+ const rafRef = React2.useRef(null);
4108
+ React2.useEffect(() => {
3778
4109
  if (isOpen) {
3779
4110
  const loadedBindings = {};
3780
4111
  for (let i = 1; i <= 4; i++) {
@@ -3786,7 +4117,7 @@ function GamepadMapper({
3786
4117
  }
3787
4118
  }
3788
4119
  }, [isOpen, gamepads]);
3789
- React5.useEffect(() => {
4120
+ React2.useEffect(() => {
3790
4121
  if (!isOpen || !listeningFor) {
3791
4122
  if (rafRef.current) {
3792
4123
  cancelAnimationFrame(rafRef.current);
@@ -3821,7 +4152,7 @@ function GamepadMapper({
3821
4152
  }
3822
4153
  };
3823
4154
  }, [isOpen, listeningFor, selectedPlayer]);
3824
- React5.useEffect(() => {
4155
+ React2.useEffect(() => {
3825
4156
  if (!isOpen) return;
3826
4157
  const handleKeyDown = (e) => {
3827
4158
  if (e.code === "Escape") {
@@ -3866,8 +4197,8 @@ function GamepadMapper({
3866
4197
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
3867
4198
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { className: "text-retro-primary", size: 24, style: { color: systemColor } }),
3868
4199
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3869
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: "Gamepad Settings" }),
3870
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: gamepads.length > 0 ? `${gamepads.length} controller${gamepads.length > 1 ? "s" : ""} connected` : "No controllers detected" })
4200
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.gamepad.title }),
4201
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none })
3871
4202
  ] })
3872
4203
  ] }),
3873
4204
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3881,7 +4212,7 @@ function GamepadMapper({
3881
4212
  ] }),
3882
4213
  gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
3883
4214
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3884
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: "Player:" }),
4215
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
3885
4216
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
3886
4217
  "button",
3887
4218
  {
@@ -3914,10 +4245,10 @@ function GamepadMapper({
3914
4245
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
3915
4246
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
3916
4247
  ] }),
3917
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: "No controller detected" }),
3918
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: "Press any button on your gamepad to connect" }),
4248
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
4249
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
3919
4250
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
3920
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: "Waiting for input..." }),
4251
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
3921
4252
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
3922
4253
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
3923
4254
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
@@ -3927,11 +4258,8 @@ function GamepadMapper({
3927
4258
  ] }),
3928
4259
  gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
3929
4260
  listeningFor && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
3930
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-white mb-1", children: [
3931
- "Press a button on your controller for ",
3932
- /* @__PURE__ */ jsxRuntime.jsx("strong", { children: BUTTON_LABELS[listeningFor] })
3933
- ] }),
3934
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: "Press Escape to cancel" })
4261
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
4262
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
3935
4263
  ] }),
3936
4264
  BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3937
4265
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
@@ -3963,7 +4291,7 @@ function GamepadMapper({
3963
4291
  backgroundColor: `${systemColor}30`,
3964
4292
  color: systemColor
3965
4293
  } : {},
3966
- children: listeningFor === btn ? "Press..." : formatGamepadButton(currentBindings[btn])
4294
+ children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
3967
4295
  }
3968
4296
  )
3969
4297
  ]
@@ -3981,7 +4309,7 @@ function GamepadMapper({
3981
4309
  className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-50",
3982
4310
  children: [
3983
4311
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
3984
- "Reset to Default"
4312
+ t.modals.gamepad.reset
3985
4313
  ]
3986
4314
  }
3987
4315
  ),
@@ -3994,7 +4322,7 @@ function GamepadMapper({
3994
4322
  style: { backgroundColor: systemColor },
3995
4323
  children: [
3996
4324
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
3997
- "Save Settings"
4325
+ t.modals.gamepad.save
3998
4326
  ]
3999
4327
  }
4000
4328
  )
@@ -4009,7 +4337,8 @@ function CheatModal({
4009
4337
  onToggle,
4010
4338
  onClose
4011
4339
  }) {
4012
- const [copiedId, setCopiedId] = React5__default.default.useState(null);
4340
+ const t = useKoinTranslation();
4341
+ const [copiedId, setCopiedId] = React2__default.default.useState(null);
4013
4342
  if (!isOpen) return null;
4014
4343
  const handleCopy = async (code, id) => {
4015
4344
  await navigator.clipboard.writeText(code);
@@ -4029,13 +4358,8 @@ function CheatModal({
4029
4358
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4030
4359
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { className: "text-purple-400", size: 24 }),
4031
4360
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4032
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: "Cheat Codes" }),
4033
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-400", children: [
4034
- cheats.length,
4035
- " cheat",
4036
- cheats.length !== 1 ? "s" : "",
4037
- " available"
4038
- ] })
4361
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.cheats.title }),
4362
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()) })
4039
4363
  ] })
4040
4364
  ] }),
4041
4365
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4049,8 +4373,8 @@ function CheatModal({
4049
4373
  ] }),
4050
4374
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-gray-500", children: [
4051
4375
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4052
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "No cheats available" }),
4053
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: "No cheat codes found for this game" })
4376
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4377
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
4054
4378
  ] }) : cheats.map((cheat) => {
4055
4379
  const isActive = activeCheats.has(cheat.id);
4056
4380
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4084,7 +4408,7 @@ function CheatModal({
4084
4408
  handleCopy(cheat.code, cheat.id);
4085
4409
  },
4086
4410
  className: "p-1.5 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors",
4087
- title: "Copy code",
4411
+ title: t.modals.cheats.copy,
4088
4412
  children: copiedId === cheat.id ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-green-400" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { size: 14 })
4089
4413
  }
4090
4414
  )
@@ -4095,7 +4419,7 @@ function CheatModal({
4095
4419
  cheat.id
4096
4420
  );
4097
4421
  }) }),
4098
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center", children: activeCheats.size > 0 ? `${activeCheats.size} cheat${activeCheats.size !== 1 ? "s" : ""} active` : "Click a cheat to toggle it on/off" }) })
4422
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center", children: activeCheats.size > 0 ? t.modals.cheats.active.replace("{{count}}", activeCheats.size.toString()) : t.modals.cheats.toggleHint }) })
4099
4423
  ] })
4100
4424
  ] });
4101
4425
  }
@@ -4108,18 +4432,22 @@ function formatBytes(bytes) {
4108
4432
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
4109
4433
  }
4110
4434
  function formatTimestamp(timestamp) {
4111
- const date = new Date(timestamp);
4112
- const now = /* @__PURE__ */ new Date();
4113
- const diff = now.getTime() - date.getTime();
4114
- if (diff < 6e4) return "Just now";
4115
- if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
4116
- if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
4117
- return date.toLocaleDateString("en-US", {
4118
- month: "short",
4119
- day: "numeric",
4120
- hour: "2-digit",
4121
- minute: "2-digit"
4122
- });
4435
+ try {
4436
+ const date = new Date(timestamp);
4437
+ const now = /* @__PURE__ */ new Date();
4438
+ const diff = now.getTime() - date.getTime();
4439
+ if (diff < 6e4) return "Just now";
4440
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
4441
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
4442
+ return date.toLocaleDateString("en-US", {
4443
+ month: "short",
4444
+ day: "numeric",
4445
+ hour: "2-digit",
4446
+ minute: "2-digit"
4447
+ });
4448
+ } catch {
4449
+ return "Unknown";
4450
+ }
4123
4451
  }
4124
4452
  function SaveSlotModal({
4125
4453
  isOpen,
@@ -4133,6 +4461,7 @@ function SaveSlotModal({
4133
4461
  maxSlots = 5,
4134
4462
  onUpgrade
4135
4463
  }) {
4464
+ const t = useKoinTranslation();
4136
4465
  if (!isOpen) return null;
4137
4466
  const isSaveMode = mode === "save";
4138
4467
  const allSlots = [1, 2, 3, 4, 5];
@@ -4161,8 +4490,8 @@ function SaveSlotModal({
4161
4490
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4162
4491
  isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
4163
4492
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4164
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? "Save Game" : "Load Game" }),
4165
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? "Choose a slot to save your progress" : "Select a save to restore" })
4493
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle }),
4494
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad })
4166
4495
  ] })
4167
4496
  ] }),
4168
4497
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4176,7 +4505,7 @@ function SaveSlotModal({
4176
4505
  ] }),
4177
4506
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
4178
4507
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
4179
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: "Loading saves..." })
4508
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
4180
4509
  ] }) : displaySlots.map((slotNum) => {
4181
4510
  const slotData = getSlotData(slotNum);
4182
4511
  const isEmpty = !slotData;
@@ -4193,14 +4522,10 @@ function SaveSlotModal({
4193
4522
  children: [
4194
4523
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-12 h-12 rounded-lg flex items-center justify-center font-bold text-xl bg-gray-700 text-gray-500", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lock, { size: 20 }) }),
4195
4524
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4196
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "font-medium text-gray-400 group-hover:text-white transition-colors", children: [
4197
- "Slot ",
4198
- slotNum,
4199
- " Locked"
4200
- ] }),
4525
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium text-gray-400 group-hover:text-white transition-colors", children: t.modals.saveSlots.locked.replace("{{num}}", slotNum.toString()) }),
4201
4526
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 group-hover:text-retro-primary transition-colors flex items-center gap-1", children: [
4202
4527
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 10 }),
4203
- "Upgrade to unlock more save slots"
4528
+ t.modals.saveSlots.upgrade
4204
4529
  ] })
4205
4530
  ] })
4206
4531
  ]
@@ -4224,8 +4549,8 @@ function SaveSlotModal({
4224
4549
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: isAutoSaveSlot ? (
4225
4550
  // Auto-save slot - special display
4226
4551
  isEmpty ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-retro-primary/70", children: [
4227
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "Auto-Save Slot" }),
4228
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-retro-primary/50", children: "Reserved for automatic saves" })
4552
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.saveSlots.autoSave }),
4553
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-retro-primary/50", children: t.modals.saveSlots.autoSaveDesc })
4229
4554
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4230
4555
  slotData.screenshot && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-16 h-12 rounded border border-retro-primary/30 overflow-hidden bg-black", children: /* @__PURE__ */ jsxRuntime.jsx(
4231
4556
  "img",
@@ -4239,7 +4564,7 @@ function SaveSlotModal({
4239
4564
  }
4240
4565
  ) }),
4241
4566
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4242
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium text-retro-primary truncate", children: "Auto-Save" }),
4567
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium text-retro-primary truncate", children: t.modals.saveSlots.autoSave }),
4243
4568
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-xs text-retro-primary/60 mt-1", children: [
4244
4569
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
4245
4570
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { size: 12 }),
@@ -4253,8 +4578,8 @@ function SaveSlotModal({
4253
4578
  ] })
4254
4579
  ] })
4255
4580
  ) : isEmpty ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-gray-500", children: [
4256
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "Empty Slot" }),
4257
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs", children: "No save data" })
4581
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.saveSlots.emptySlot }),
4582
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs", children: t.modals.saveSlots.noData })
4258
4583
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4259
4584
  slotData.screenshot && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-16 h-12 rounded border border-white/10 overflow-hidden bg-black", children: /* @__PURE__ */ jsxRuntime.jsx(
4260
4585
  "img",
@@ -4268,10 +4593,7 @@ function SaveSlotModal({
4268
4593
  }
4269
4594
  ) }),
4270
4595
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4271
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "font-medium text-white truncate", children: [
4272
- "Slot ",
4273
- slotNum
4274
- ] }),
4596
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium text-white truncate", children: t.modals.saveSlots.slot.replace("{{num}}", slotNum.toString()) }),
4275
4597
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-xs text-gray-400 mt-1", children: [
4276
4598
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
4277
4599
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { size: 12 }),
@@ -4301,7 +4623,7 @@ function SaveSlotModal({
4301
4623
  slotNum
4302
4624
  );
4303
4625
  }) }),
4304
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center", children: isSaveMode ? "Saves are stored in the cloud and sync across devices" : "Your progress will be restored to the selected save point" }) })
4626
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }) })
4305
4627
  ] })
4306
4628
  ] });
4307
4629
  }
@@ -4313,6 +4635,7 @@ function BiosSelectionModal({
4313
4635
  onSelectBios,
4314
4636
  systemColor = "#00FF41"
4315
4637
  }) {
4638
+ const t = useKoinTranslation();
4316
4639
  if (!isOpen) return null;
4317
4640
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4318
4641
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4332,8 +4655,8 @@ function BiosSelectionModal({
4332
4655
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4333
4656
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Cpu, { size: 24, style: { color: systemColor } }),
4334
4657
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4335
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white uppercase tracking-wide", children: "BIOS Selection" }),
4336
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: "Select a BIOS file to use for this game" })
4658
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white uppercase tracking-wide", children: t.modals.bios.title }),
4659
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.bios.description })
4337
4660
  ] })
4338
4661
  ] }),
4339
4662
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4349,8 +4672,9 @@ function BiosSelectionModal({
4349
4672
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 p-3 rounded bg-yellow-500/10 border border-yellow-500/20 flex items-start gap-3", children: [
4350
4673
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "shrink-0 text-yellow-500 mt-0.5", size: 16 }),
4351
4674
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-yellow-200/80", children: [
4352
- /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Note:" }),
4353
- " Changing BIOS requires the emulator to restart. Your unsaved progress will be lost."
4675
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: t.modals.bios.warningTitle }),
4676
+ " ",
4677
+ t.modals.bios.warning
4354
4678
  ] })
4355
4679
  ] }),
4356
4680
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4380,10 +4704,10 @@ function BiosSelectionModal({
4380
4704
  ),
4381
4705
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4382
4706
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4383
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: `font-mono font-bold truncate ${!currentBiosId ? "text-white" : "text-gray-300"}`, children: "System Default" }),
4384
- !currentBiosId && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-1.5 py-0.5 rounded text-[10px] font-bold bg-white/20 text-white uppercase tracking-wider", children: "Active" })
4707
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `font-mono font-bold truncate ${!currentBiosId ? "text-white" : "text-gray-300"}`, children: t.modals.bios.systemDefault }),
4708
+ !currentBiosId && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-1.5 py-0.5 rounded text-[10px] font-bold bg-white/20 text-white uppercase tracking-wider", children: t.modals.bios.active })
4385
4709
  ] }),
4386
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 truncate mt-0.5", children: "Use the emulator's built-in or default BIOS" })
4710
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 truncate mt-0.5", children: t.modals.bios.defaultDesc })
4387
4711
  ] }),
4388
4712
  !currentBiosId && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 20, style: { color: systemColor } })
4389
4713
  ]
@@ -4391,8 +4715,8 @@ function BiosSelectionModal({
4391
4715
  ),
4392
4716
  biosOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-8 text-gray-500", children: [
4393
4717
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileCode, { size: 48, className: "mx-auto mb-3 opacity-30" }),
4394
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "No BIOS files found for this console." }),
4395
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] mt-1 text-gray-600", children: "Upload BIOS files in your Dashboard Library." })
4718
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: t.modals.bios.emptyTitle }),
4719
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] mt-1 text-gray-600", children: t.modals.bios.emptyDesc })
4396
4720
  ] }) : biosOptions.map((bios) => {
4397
4721
  const isSelected = bios.id === currentBiosId;
4398
4722
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4423,7 +4747,7 @@ function BiosSelectionModal({
4423
4747
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4424
4748
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4425
4749
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: `font-mono font-bold truncate ${isSelected ? "text-white" : "text-gray-300"}`, children: bios.name }),
4426
- isSelected && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-1.5 py-0.5 rounded text-[10px] font-bold bg-white/20 text-white uppercase tracking-wider", children: "Active" })
4750
+ isSelected && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-1.5 py-0.5 rounded text-[10px] font-bold bg-white/20 text-white uppercase tracking-wider", children: t.modals.bios.active })
4427
4751
  ] }),
4428
4752
  bios.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 truncate mt-0.5", children: bios.description })
4429
4753
  ] }),
@@ -4434,12 +4758,84 @@ function BiosSelectionModal({
4434
4758
  );
4435
4759
  })
4436
4760
  ] }),
4437
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-gray-500 uppercase tracking-widest", children: "System Firmware Settings" }) })
4761
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-gray-500 uppercase tracking-widest", children: t.modals.bios.footer }) })
4438
4762
  ]
4439
4763
  }
4440
4764
  )
4441
4765
  ] });
4442
4766
  }
4767
+ function SettingsModal({
4768
+ isOpen,
4769
+ onClose,
4770
+ currentLanguage,
4771
+ onLanguageChange,
4772
+ systemColor = "#00FF41"
4773
+ }) {
4774
+ const t = useKoinTranslation();
4775
+ if (!isOpen) return null;
4776
+ const languages = [
4777
+ { code: "en", name: "English" },
4778
+ { code: "es", name: "Espa\xF1ol" },
4779
+ { code: "fr", name: "Fran\xE7ais" }
4780
+ ];
4781
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4782
+ /* @__PURE__ */ jsxRuntime.jsx(
4783
+ "div",
4784
+ {
4785
+ className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
4786
+ onClick: onClose
4787
+ }
4788
+ ),
4789
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative bg-gray-900 border border-white/10 rounded-xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden animate-in fade-in zoom-in-95 duration-200", children: [
4790
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
4791
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4792
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { className: "text-white", size: 20 }),
4793
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.settings.title })
4794
+ ] }),
4795
+ /* @__PURE__ */ jsxRuntime.jsx(
4796
+ "button",
4797
+ {
4798
+ onClick: onClose,
4799
+ className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
4800
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
4801
+ }
4802
+ )
4803
+ ] }),
4804
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
4805
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
4806
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
4807
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
4808
+ ] }),
4809
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-2", children: languages.map((lang) => {
4810
+ const isActive = currentLanguage === lang.code;
4811
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4812
+ "button",
4813
+ {
4814
+ onClick: () => onLanguageChange(lang.code),
4815
+ className: `
4816
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4817
+ ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
4818
+ `,
4819
+ children: [
4820
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
4821
+ isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
4822
+ ]
4823
+ },
4824
+ lang.code
4825
+ );
4826
+ }) })
4827
+ ] }) }),
4828
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 bg-black/30 border-t border-white/10 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(
4829
+ "button",
4830
+ {
4831
+ onClick: onClose,
4832
+ className: "text-sm text-gray-500 hover:text-white transition-colors",
4833
+ children: t.modals.shortcuts.pressEsc
4834
+ }
4835
+ ) })
4836
+ ] })
4837
+ ] });
4838
+ }
4443
4839
  function GameModals({
4444
4840
  controlsModalOpen,
4445
4841
  setControlsModalOpen,
@@ -4471,7 +4867,11 @@ function GameModals({
4471
4867
  setBiosModalOpen,
4472
4868
  availableBios,
4473
4869
  currentBiosId,
4474
- onSelectBios
4870
+ onSelectBios,
4871
+ settingsModalOpen,
4872
+ setSettingsModalOpen,
4873
+ currentLanguage,
4874
+ onLanguageChange
4475
4875
  }) {
4476
4876
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4477
4877
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4547,6 +4947,19 @@ function GameModals({
4547
4947
  },
4548
4948
  systemColor
4549
4949
  }
4950
+ ),
4951
+ /* @__PURE__ */ jsxRuntime.jsx(
4952
+ SettingsModal,
4953
+ {
4954
+ isOpen: settingsModalOpen,
4955
+ onClose: () => {
4956
+ setSettingsModalOpen(false);
4957
+ onResume();
4958
+ },
4959
+ currentLanguage,
4960
+ onLanguageChange,
4961
+ systemColor
4962
+ }
4550
4963
  )
4551
4964
  ] });
4552
4965
  }
@@ -4561,10 +4974,11 @@ function LoginForm({
4561
4974
  error,
4562
4975
  onSubmit
4563
4976
  }) {
4977
+ const t = useKoinTranslation();
4564
4978
  return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit, className: "p-4 space-y-3", children: [
4565
4979
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mb-4", children: [
4566
4980
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 mx-auto mb-3 rounded-full bg-gradient-to-br from-yellow-500/20 to-orange-500/20 border border-yellow-500/30 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trophy, { className: "text-yellow-400", size: 32 }) }),
4567
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-xs", children: "Connect your account to track achievements" }),
4981
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-xs", children: t.retroAchievements.connectAccount }),
4568
4982
  /* @__PURE__ */ jsxRuntime.jsxs(
4569
4983
  "a",
4570
4984
  {
@@ -4573,14 +4987,14 @@ function LoginForm({
4573
4987
  rel: "noopener noreferrer",
4574
4988
  className: "text-yellow-400 hover:text-yellow-300 text-xs inline-flex items-center gap-1 mt-1",
4575
4989
  children: [
4576
- "Create an account",
4990
+ t.retroAchievements.createAccount,
4577
4991
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { size: 10 })
4578
4992
  ]
4579
4993
  }
4580
4994
  )
4581
4995
  ] }),
4582
4996
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4583
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-[10px] text-gray-500 mb-1 uppercase tracking-wider", children: "Username" }),
4997
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-[10px] text-gray-500 mb-1 uppercase tracking-wider", children: t.retroAchievements.username }),
4584
4998
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
4585
4999
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-500", size: 14 }),
4586
5000
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4589,7 +5003,7 @@ function LoginForm({
4589
5003
  type: "text",
4590
5004
  value: username,
4591
5005
  onChange: (e) => setUsername(e.target.value),
4592
- placeholder: "Your RA username",
5006
+ placeholder: t.retroAchievements.yourUsername,
4593
5007
  className: "w-full pl-8 pr-3 py-2 text-sm bg-black/50 border border-white/20 rounded-lg text-white placeholder-gray-500 focus:border-yellow-500/50 focus:outline-none transition-colors",
4594
5008
  disabled: isLoading
4595
5009
  }
@@ -4597,7 +5011,7 @@ function LoginForm({
4597
5011
  ] })
4598
5012
  ] }),
4599
5013
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4600
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-[10px] text-gray-500 mb-1 uppercase tracking-wider", children: "Password" }),
5014
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-[10px] text-gray-500 mb-1 uppercase tracking-wider", children: t.retroAchievements.password }),
4601
5015
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
4602
5016
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lock, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-500", size: 14 }),
4603
5017
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4606,7 +5020,7 @@ function LoginForm({
4606
5020
  type: showPassword ? "text" : "password",
4607
5021
  value: password,
4608
5022
  onChange: (e) => setPassword(e.target.value),
4609
- placeholder: "Your RA password",
5023
+ placeholder: t.retroAchievements.yourPassword,
4610
5024
  className: "w-full pl-8 pr-9 py-2 text-sm bg-black/50 border border-white/20 rounded-lg text-white placeholder-gray-500 focus:border-yellow-500/50 focus:outline-none transition-colors",
4611
5025
  disabled: isLoading
4612
5026
  }
@@ -4634,16 +5048,20 @@ function LoginForm({
4634
5048
  className: "w-full flex items-center justify-center gap-2 px-3 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 hover:from-yellow-400 hover:to-orange-400 text-black text-sm font-bold rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed",
4635
5049
  children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4636
5050
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "animate-spin", size: 14 }),
4637
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Connecting..." })
5051
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t.retroAchievements.connecting })
4638
5052
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4639
5053
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trophy, { size: 14 }),
4640
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Connect" })
5054
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t.retroAchievements.login })
4641
5055
  ] })
4642
5056
  }
4643
5057
  ),
4644
5058
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 bg-blue-500/10 border border-blue-500/20 rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[10px] text-blue-300 leading-relaxed", children: [
4645
- /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "\u{1F512} Privacy:" }),
4646
- " Your password is only used to authenticate with RetroAchievements. It is never stored."
5059
+ /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
5060
+ "\u{1F512} ",
5061
+ t.retroAchievements.privacy
5062
+ ] }),
5063
+ " ",
5064
+ t.retroAchievements.privacyText
4647
5065
  ] }) })
4648
5066
  ] });
4649
5067
  }
@@ -4667,6 +5085,7 @@ function SettingsTab({
4667
5085
  progress,
4668
5086
  onLogout
4669
5087
  }) {
5088
+ const t = useKoinTranslation();
4670
5089
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
4671
5090
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 p-3 bg-black/30 rounded-lg border border-yellow-500/20", children: [
4672
5091
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-12 h-12 rounded-lg bg-gray-800 overflow-hidden border-2 border-yellow-500/50 flex-shrink-0", children: credentials.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -4736,7 +5155,7 @@ function SettingsTab({
4736
5155
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4737
5156
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Shield, { className: hardcoreEnabled ? "text-red-500" : "text-gray-500", size: 16 }),
4738
5157
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4739
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-white", children: "Hardcore Mode" }),
5158
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-white", children: t.retroAchievements.hardcore }),
4740
5159
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-gray-500", children: "2x points, no savestates" })
4741
5160
  ] })
4742
5161
  ] }),
@@ -4754,11 +5173,11 @@ function SettingsTab({
4754
5173
  }
4755
5174
  )
4756
5175
  ] }),
4757
- hardcoreEnabled && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 p-2 bg-red-500/10 border border-red-500/20 rounded text-[10px] text-red-300", children: "Savestates, rewind, cheats & slow-mo disabled" })
5176
+ hardcoreEnabled && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 p-2 bg-red-500/10 border border-red-500/20 rounded text-[10px] text-red-300", children: t.common.disabledInHardcore })
4758
5177
  ] }),
4759
5178
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs px-1", children: [
4760
5179
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle, { className: "text-green-500", size: 12 }),
4761
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-green-400", children: "Connected" })
5180
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-green-400", children: t.retroAchievements.connectedStatus })
4762
5181
  ] }),
4763
5182
  /* @__PURE__ */ jsxRuntime.jsxs(
4764
5183
  "button",
@@ -4767,7 +5186,7 @@ function SettingsTab({
4767
5186
  className: "w-full flex items-center justify-center gap-2 px-3 py-2 bg-gray-800 hover:bg-gray-700 text-white text-xs rounded-lg transition-colors border border-white/10",
4768
5187
  children: [
4769
5188
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LogOut, { size: 14 }),
4770
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Sign Out" })
5189
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t.retroAchievements.logout })
4771
5190
  ]
4772
5191
  }
4773
5192
  )
@@ -4784,18 +5203,19 @@ function AchievementsTab({
4784
5203
  earnedPoints,
4785
5204
  totalPoints
4786
5205
  }) {
5206
+ const t = useKoinTranslation();
4787
5207
  if (!currentGame) {
4788
5208
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 text-center", children: [
4789
5209
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-12 h-12 mx-auto mb-3 rounded-full bg-gray-800 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trophy, { className: "text-gray-600", size: 24 }) }),
4790
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-400", children: "No game loaded" }),
4791
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: "Load a game to see achievements" })
5210
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-400", children: t.retroAchievements.noGame }),
5211
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: t.retroAchievements.loadGame })
4792
5212
  ] });
4793
5213
  }
4794
5214
  if (achievements.length === 0) {
4795
5215
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 text-center", children: [
4796
5216
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-12 h-12 mx-auto mb-3 rounded-full bg-gray-800 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trophy, { className: "text-gray-600", size: 24 }) }),
4797
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-400", children: "No achievements found" }),
4798
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: "This game may not be supported" })
5217
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-400", children: t.retroAchievements.noAchievements }),
5218
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: t.retroAchievements.notSupported })
4799
5219
  ] });
4800
5220
  }
4801
5221
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
@@ -4848,7 +5268,7 @@ function AchievementsTab({
4848
5268
  {
4849
5269
  onClick: () => setFilter(f),
4850
5270
  className: `px-2 py-0.5 text-[10px] rounded-full border transition-colors ${filter === f ? "bg-yellow-500/20 border-yellow-500/50 text-yellow-400" : "border-gray-700 text-gray-500 hover:text-gray-300"}`,
4851
- children: f.charAt(0).toUpperCase() + f.slice(1)
5271
+ children: t.retroAchievements.filters[f]
4852
5272
  },
4853
5273
  f
4854
5274
  )) })
@@ -4887,8 +5307,7 @@ function AchievementsTab({
4887
5307
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-2 text-[10px] text-gray-500 text-center border-t border-gray-800/50", children: [
4888
5308
  progress,
4889
5309
  "% complete \u2022 ",
4890
- totalPoints - earnedPoints,
4891
- " pts remaining"
5310
+ t.retroAchievements.ptsRemaining.replace("{{count}}", (totalPoints - earnedPoints).toString())
4892
5311
  ] })
4893
5312
  ] });
4894
5313
  }
@@ -4907,14 +5326,15 @@ function RASidebar({
4907
5326
  achievements,
4908
5327
  unlockedIds
4909
5328
  }) {
5329
+ const t = useKoinTranslation();
4910
5330
  const defaultTab = isLoggedIn && currentGame && achievements.length > 0 ? "achievements" : "settings";
4911
- const [activeTab, setActiveTab] = React5.useState(defaultTab);
4912
- const [filter, setFilter] = React5.useState("all");
4913
- const [username, setUsername] = React5.useState("");
4914
- const [password, setPassword] = React5.useState("");
4915
- const [showPassword, setShowPassword] = React5.useState(false);
4916
- const [localError, setLocalError] = React5.useState(null);
4917
- React5.useEffect(() => {
5331
+ const [activeTab, setActiveTab] = React2.useState(defaultTab);
5332
+ const [filter, setFilter] = React2.useState("all");
5333
+ const [username, setUsername] = React2.useState("");
5334
+ const [password, setPassword] = React2.useState("");
5335
+ const [showPassword, setShowPassword] = React2.useState(false);
5336
+ const [localError, setLocalError] = React2.useState(null);
5337
+ React2.useEffect(() => {
4918
5338
  if (!isOpen) {
4919
5339
  setUsername("");
4920
5340
  setPassword("");
@@ -4932,7 +5352,7 @@ function RASidebar({
4932
5352
  e.preventDefault();
4933
5353
  setLocalError(null);
4934
5354
  if (!username.trim() || !password.trim()) {
4935
- setLocalError("Username and Password are required");
5355
+ setLocalError(t.retroAchievements.usernameRequired);
4936
5356
  return;
4937
5357
  }
4938
5358
  const success = await onLogin(username.trim(), password.trim());
@@ -4975,7 +5395,7 @@ function RASidebar({
4975
5395
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
4976
5396
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4977
5397
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trophy, { className: "text-yellow-400", size: 18 }),
4978
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-heading text-sm text-white", children: "RetroAchievements" })
5398
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-heading text-sm text-white", children: t.retroAchievements.title })
4979
5399
  ] }),
4980
5400
  /* @__PURE__ */ jsxRuntime.jsx(
4981
5401
  "button",
@@ -4994,7 +5414,7 @@ function RASidebar({
4994
5414
  className: `flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs rounded-md transition-colors ${activeTab === "achievements" ? "bg-yellow-500/20 text-yellow-400" : "text-gray-400 hover:text-white"}`,
4995
5415
  children: [
4996
5416
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { size: 12 }),
4997
- "Achievements"
5417
+ t.retroAchievements.achievements
4998
5418
  ]
4999
5419
  }
5000
5420
  ),
@@ -5005,7 +5425,7 @@ function RASidebar({
5005
5425
  className: `flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs rounded-md transition-colors ${activeTab === "settings" ? "bg-yellow-500/20 text-yellow-400" : "text-gray-400 hover:text-white"}`,
5006
5426
  children: [
5007
5427
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { size: 12 }),
5008
- "Settings"
5428
+ t.settings.title
5009
5429
  ]
5010
5430
  }
5011
5431
  )
@@ -5052,7 +5472,7 @@ function RASidebar({
5052
5472
  }
5053
5473
  ) }),
5054
5474
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-2 border-t border-gray-800 bg-gray-900/80 text-[10px] text-gray-500 text-center flex-shrink-0", children: [
5055
- "Powered by",
5475
+ t.retroAchievements.poweredBy,
5056
5476
  " ",
5057
5477
  /* @__PURE__ */ jsxRuntime.jsx(
5058
5478
  "a",
@@ -5069,8 +5489,8 @@ function RASidebar({
5069
5489
  ] });
5070
5490
  }
5071
5491
  function useToast(defaultDuration = 3e3) {
5072
- const [toasts, setToasts] = React5.useState([]);
5073
- const showToast = React5.useCallback((message, type = "info", options) => {
5492
+ const [toasts, setToasts] = React2.useState([]);
5493
+ const showToast = React2.useCallback((message, type = "info", options) => {
5074
5494
  const id = crypto.randomUUID();
5075
5495
  const duration = options?.duration ?? defaultDuration;
5076
5496
  const newToast = {
@@ -5089,10 +5509,10 @@ function useToast(defaultDuration = 3e3) {
5089
5509
  }, duration);
5090
5510
  }
5091
5511
  }, [defaultDuration]);
5092
- const dismissToast = React5.useCallback((id) => {
5512
+ const dismissToast = React2.useCallback((id) => {
5093
5513
  setToasts((prev) => prev.filter((t) => t.id !== id));
5094
5514
  }, []);
5095
- const clearToasts = React5.useCallback(() => {
5515
+ const clearToasts = React2.useCallback(() => {
5096
5516
  setToasts([]);
5097
5517
  }, []);
5098
5518
  return {
@@ -5106,21 +5526,21 @@ function useToast(defaultDuration = 3e3) {
5106
5526
  // src/hooks/useGameUI.ts
5107
5527
  function useGameUI() {
5108
5528
  const { isMobile } = useMobile();
5109
- const containerRef = React5.useRef(null);
5110
- const canvasRef = React5.useRef(null);
5529
+ const containerRef = React2.useRef(null);
5530
+ const canvasRef = React2.useRef(null);
5111
5531
  const { toasts, showToast, dismissToast } = useToast(3500);
5112
- const [isFullscreen2, setIsFullscreen] = React5.useState(false);
5113
- const [raSidebarOpen, setRaSidebarOpen] = React5.useState(false);
5114
- const checkFullscreen = React5.useCallback(() => {
5532
+ const [isFullscreen2, setIsFullscreen] = React2.useState(false);
5533
+ const [raSidebarOpen, setRaSidebarOpen] = React2.useState(false);
5534
+ const checkFullscreen = React2.useCallback(() => {
5115
5535
  const fullscreen = isFullscreen();
5116
5536
  setIsFullscreen(fullscreen);
5117
5537
  return fullscreen;
5118
5538
  }, []);
5119
- React5.useEffect(() => {
5539
+ React2.useEffect(() => {
5120
5540
  checkFullscreen();
5121
5541
  return setupFullscreenListener(checkFullscreen);
5122
5542
  }, [checkFullscreen]);
5123
- const handleFullscreen = React5.useCallback(async () => {
5543
+ const handleFullscreen = React2.useCallback(async () => {
5124
5544
  if (!containerRef.current) return;
5125
5545
  try {
5126
5546
  const nativeWorked = await toggleFullscreen(containerRef.current);
@@ -5662,22 +6082,36 @@ async function getCachedRom(romId) {
5662
6082
  }
5663
6083
  return null;
5664
6084
  }
6085
+ var pendingFetches = /* @__PURE__ */ new Map();
5665
6086
  async function fetchAndCacheRom(romId, url) {
5666
- const response = await fetch(url);
5667
- if (!response.ok) {
5668
- throw new Error(`Failed to fetch ROM: ${response.statusText}`);
6087
+ if (pendingFetches.has(romId)) {
6088
+ console.log(`[Cache] Joining in-flight fetch for ${romId}`);
6089
+ return pendingFetches.get(romId);
5669
6090
  }
5670
- const blob = await response.blob();
5671
- if (typeof caches !== "undefined") {
6091
+ const fetchPromise = (async () => {
5672
6092
  try {
5673
- const cache = await caches.open(CACHE_NAME);
5674
- await cache.put(romId, new Response(blob));
5675
- console.log(`[Cache] Cached ROM ${romId}`);
5676
- } catch (e) {
5677
- console.warn("[Cache] Write failed:", e);
6093
+ const cached = await getCachedRom(romId);
6094
+ if (cached) return cached;
6095
+ console.log(`[Cache] Fetching ROM ${romId}...`);
6096
+ const response = await fetch(url);
6097
+ if (!response.ok) throw new Error(`Failed to fetch ROM: ${response.statusText}`);
6098
+ const blob = await response.blob();
6099
+ if (typeof caches !== "undefined") {
6100
+ try {
6101
+ const cache = await caches.open(CACHE_NAME);
6102
+ await cache.put(romId, new Response(blob));
6103
+ console.log(`[Cache] Cached ROM ${romId}`);
6104
+ } catch (e) {
6105
+ console.warn("[Cache] Write failed:", e);
6106
+ }
6107
+ }
6108
+ return blob;
6109
+ } finally {
6110
+ pendingFetches.delete(romId);
5678
6111
  }
5679
- }
5680
- return blob;
6112
+ })();
6113
+ pendingFetches.set(romId, fetchPromise);
6114
+ return fetchPromise;
5681
6115
  }
5682
6116
 
5683
6117
  // src/hooks/emulator/useEmulatorCore.ts
@@ -5698,15 +6132,15 @@ function useEmulatorCore({
5698
6132
  onReady,
5699
6133
  onError
5700
6134
  }) {
5701
- const [status, setStatus] = React5.useState("idle");
5702
- const [error, setError] = React5.useState(null);
5703
- const [isPaused, setIsPaused] = React5.useState(false);
5704
- const [speed, setSpeedState] = React5.useState(1);
5705
- const [isFastForwardOn, setIsFastForwardOn] = React5.useState(false);
5706
- const [isPerformanceMode, setIsPerformanceMode] = React5.useState(false);
5707
- const nostalgistRef = React5.useRef(null);
5708
- const isStartingRef = React5.useRef(false);
5709
- const prepare = React5.useCallback(async () => {
6135
+ const [status, setStatus] = React2.useState("idle");
6136
+ const [error, setError] = React2.useState(null);
6137
+ const [isPaused, setIsPaused] = React2.useState(false);
6138
+ const [speed, setSpeedState] = React2.useState(1);
6139
+ const [isFastForwardOn, setIsFastForwardOn] = React2.useState(false);
6140
+ const [isPerformanceMode, setIsPerformanceMode] = React2.useState(false);
6141
+ const nostalgistRef = React2.useRef(null);
6142
+ const isStartingRef = React2.useRef(false);
6143
+ const prepare = React2.useCallback(async () => {
5710
6144
  if (!romUrl || !system) {
5711
6145
  console.warn("[Nostalgist] Missing romUrl or system");
5712
6146
  return;
@@ -5855,7 +6289,7 @@ function useEmulatorCore({
5855
6289
  onError?.(err instanceof Error ? err : new Error(errorMessage));
5856
6290
  }
5857
6291
  }, [system, romUrl, coreOverride, biosUrl, initialState, getCanvasElement, keyboardControls, gamepadBindings, initialVolume, onError, retroAchievements]);
5858
- const start = React5.useCallback(async () => {
6292
+ const start = React2.useCallback(async () => {
5859
6293
  if (isStartingRef.current) {
5860
6294
  console.log("[Nostalgist] Already starting");
5861
6295
  return;
@@ -5888,7 +6322,7 @@ function useEmulatorCore({
5888
6322
  isStartingRef.current = false;
5889
6323
  }
5890
6324
  }, [prepare, onReady, onError]);
5891
- const stop = React5.useCallback(() => {
6325
+ const stop = React2.useCallback(() => {
5892
6326
  if (nostalgistRef.current) {
5893
6327
  try {
5894
6328
  nostalgistRef.current.exit();
@@ -5901,7 +6335,7 @@ function useEmulatorCore({
5901
6335
  setIsPaused(false);
5902
6336
  }
5903
6337
  }, []);
5904
- React5.useEffect(() => {
6338
+ React2.useEffect(() => {
5905
6339
  return () => {
5906
6340
  if (nostalgistRef.current) {
5907
6341
  console.log("[Nostalgist] Cleaning up emulator on unmount");
@@ -5909,7 +6343,7 @@ function useEmulatorCore({
5909
6343
  }
5910
6344
  };
5911
6345
  }, [stop]);
5912
- const restart = React5.useCallback(async () => {
6346
+ const restart = React2.useCallback(async () => {
5913
6347
  if (nostalgistRef.current) {
5914
6348
  try {
5915
6349
  nostalgistRef.current.restart();
@@ -5924,7 +6358,7 @@ function useEmulatorCore({
5924
6358
  }
5925
6359
  }
5926
6360
  }, [stop, start]);
5927
- const pause = React5.useCallback(() => {
6361
+ const pause = React2.useCallback(() => {
5928
6362
  if (nostalgistRef.current && !isPaused && status === "running") {
5929
6363
  try {
5930
6364
  nostalgistRef.current.pause();
@@ -5935,7 +6369,7 @@ function useEmulatorCore({
5935
6369
  }
5936
6370
  }
5937
6371
  }, [isPaused, status]);
5938
- const resume = React5.useCallback(() => {
6372
+ const resume = React2.useCallback(() => {
5939
6373
  if (nostalgistRef.current) {
5940
6374
  try {
5941
6375
  nostalgistRef.current.resume();
@@ -5948,14 +6382,14 @@ function useEmulatorCore({
5948
6382
  }
5949
6383
  }
5950
6384
  }, [status]);
5951
- const togglePause = React5.useCallback(() => {
6385
+ const togglePause = React2.useCallback(() => {
5952
6386
  if (isPaused) {
5953
6387
  resume();
5954
6388
  } else {
5955
6389
  pause();
5956
6390
  }
5957
6391
  }, [isPaused, pause, resume]);
5958
- const setSpeed = React5.useCallback((multiplier) => {
6392
+ const setSpeed = React2.useCallback((multiplier) => {
5959
6393
  if (!nostalgistRef.current) return;
5960
6394
  try {
5961
6395
  const nostalgist = nostalgistRef.current;
@@ -5975,7 +6409,7 @@ function useEmulatorCore({
5975
6409
  console.error("[Nostalgist] Set speed error:", err);
5976
6410
  }
5977
6411
  }, [isFastForwardOn]);
5978
- const screenshot = React5.useCallback(async () => {
6412
+ const screenshot = React2.useCallback(async () => {
5979
6413
  if (!nostalgistRef.current) {
5980
6414
  return null;
5981
6415
  }
@@ -6005,7 +6439,7 @@ function useEmulatorCore({
6005
6439
  return null;
6006
6440
  }
6007
6441
  }, []);
6008
- const resize = React5.useCallback((size) => {
6442
+ const resize = React2.useCallback((size) => {
6009
6443
  if (!nostalgistRef.current) {
6010
6444
  console.warn("[Nostalgist] Cannot resize: emulator not ready");
6011
6445
  return;
@@ -6016,7 +6450,7 @@ function useEmulatorCore({
6016
6450
  console.error("[Nostalgist] Resize error:", err);
6017
6451
  }
6018
6452
  }, []);
6019
- React5.useCallback(() => {
6453
+ React2.useCallback(() => {
6020
6454
  return nostalgistRef.current;
6021
6455
  }, []);
6022
6456
  return {
@@ -6042,11 +6476,11 @@ function useEmulatorCore({
6042
6476
  };
6043
6477
  }
6044
6478
  function useEmulatorAudio({ nostalgistRef, initialVolume = 100 }) {
6045
- const [volume, setVolume] = React5.useState(initialVolume);
6046
- const [isMuted, setIsMuted] = React5.useState(false);
6047
- const gainNodeRef = React5.useRef(null);
6048
- const lastVolumeRef = React5.useRef(initialVolume);
6049
- React5.useEffect(() => {
6479
+ const [volume, setVolume] = React2.useState(initialVolume);
6480
+ const [isMuted, setIsMuted] = React2.useState(false);
6481
+ const gainNodeRef = React2.useRef(null);
6482
+ const lastVolumeRef = React2.useRef(initialVolume);
6483
+ React2.useEffect(() => {
6050
6484
  const originalConnect = AudioNode.prototype.connect;
6051
6485
  const contextGainMap = /* @__PURE__ */ new WeakMap();
6052
6486
  AudioNode.prototype.connect = function(destination, output, input) {
@@ -6074,7 +6508,7 @@ function useEmulatorAudio({ nostalgistRef, initialVolume = 100 }) {
6074
6508
  AudioNode.prototype.connect = originalConnect;
6075
6509
  };
6076
6510
  }, []);
6077
- const setVolumeLevel = React5.useCallback((newVolume) => {
6511
+ const setVolumeLevel = React2.useCallback((newVolume) => {
6078
6512
  const clampedVolume = Math.max(0, Math.min(100, newVolume));
6079
6513
  const volumeValue = clampedVolume / 100;
6080
6514
  try {
@@ -6095,7 +6529,7 @@ function useEmulatorAudio({ nostalgistRef, initialVolume = 100 }) {
6095
6529
  console.error("[Nostalgist] Volume change error:", err);
6096
6530
  }
6097
6531
  }, []);
6098
- const toggleMute = React5.useCallback(() => {
6532
+ const toggleMute = React2.useCallback(() => {
6099
6533
  if (!nostalgistRef.current) return;
6100
6534
  try {
6101
6535
  const emscripten = nostalgistRef.current.getEmscripten();
@@ -6137,7 +6571,7 @@ function useEmulatorAudio({ nostalgistRef, initialVolume = 100 }) {
6137
6571
  };
6138
6572
  }
6139
6573
  function useEmulatorInput({ nostalgistRef }) {
6140
- const pressKey = React5.useCallback((key) => {
6574
+ const pressKey = React2.useCallback((key) => {
6141
6575
  if (!nostalgistRef.current) return;
6142
6576
  try {
6143
6577
  nostalgistRef.current.press(key);
@@ -6152,10 +6586,10 @@ function useEmulatorInput({ nostalgistRef }) {
6152
6586
  var MIN_SAVE_INTERVAL = 100;
6153
6587
  var SAVE_TIMEOUT = 5e3;
6154
6588
  function useSaveScheduler(nostalgistRef) {
6155
- const queueRef = React5.useRef([]);
6156
- const savingRef = React5.useRef(false);
6157
- const lastSaveTimeRef = React5.useRef(0);
6158
- const processQueue = React5.useCallback(async () => {
6589
+ const queueRef = React2.useRef([]);
6590
+ const savingRef = React2.useRef(false);
6591
+ const lastSaveTimeRef = React2.useRef(0);
6592
+ const processQueue = React2.useCallback(async () => {
6159
6593
  if (savingRef.current || queueRef.current.length === 0) {
6160
6594
  return;
6161
6595
  }
@@ -6209,7 +6643,7 @@ function useSaveScheduler(nostalgistRef) {
6209
6643
  setTimeout(() => processQueue(), MIN_SAVE_INTERVAL);
6210
6644
  }
6211
6645
  }, [nostalgistRef]);
6212
- const save = React5.useCallback(() => {
6646
+ const save = React2.useCallback(() => {
6213
6647
  return new Promise((resolve) => {
6214
6648
  queueRef.current.push({
6215
6649
  priority: "high",
@@ -6219,7 +6653,7 @@ function useSaveScheduler(nostalgistRef) {
6219
6653
  processQueue();
6220
6654
  });
6221
6655
  }, [processQueue]);
6222
- const queueRewindCapture = React5.useCallback(() => {
6656
+ const queueRewindCapture = React2.useCallback(() => {
6223
6657
  const lowPriorityCount = queueRef.current.filter((q) => q.priority === "low").length;
6224
6658
  if (lowPriorityCount >= 3) {
6225
6659
  return Promise.resolve(null);
@@ -6233,13 +6667,13 @@ function useSaveScheduler(nostalgistRef) {
6233
6667
  processQueue();
6234
6668
  });
6235
6669
  }, [processQueue]);
6236
- const isSaving = React5.useCallback(() => savingRef.current, []);
6237
- const getQueueLength = React5.useCallback(() => queueRef.current.length, []);
6238
- const clearQueue = React5.useCallback(() => {
6670
+ const isSaving = React2.useCallback(() => savingRef.current, []);
6671
+ const getQueueLength = React2.useCallback(() => queueRef.current.length, []);
6672
+ const clearQueue = React2.useCallback(() => {
6239
6673
  queueRef.current.forEach((item) => item.resolve(null));
6240
6674
  queueRef.current = [];
6241
6675
  }, []);
6242
- const resultRef = React5.useRef({
6676
+ const resultRef = React2.useRef({
6243
6677
  save,
6244
6678
  queueRewindCapture,
6245
6679
  isSaving,
@@ -6256,22 +6690,22 @@ function useSaveScheduler(nostalgistRef) {
6256
6690
 
6257
6691
  // src/hooks/emulator/useEmulatorSaves.ts
6258
6692
  function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rewindEnabled = true }) {
6259
- const [isRewinding, setIsRewinding] = React5.useState(false);
6260
- const [rewindBufferSize, setRewindBufferSize] = React5.useState(0);
6261
- const rewindIntervalRef = React5.useRef(null);
6262
- const rewindBufferRef = React5.useRef([]);
6263
- const rewindCaptureIntervalRef = React5.useRef(null);
6693
+ const [isRewinding, setIsRewinding] = React2.useState(false);
6694
+ const [rewindBufferSize, setRewindBufferSize] = React2.useState(0);
6695
+ const rewindIntervalRef = React2.useRef(null);
6696
+ const rewindBufferRef = React2.useRef([]);
6697
+ const rewindCaptureIntervalRef = React2.useRef(null);
6264
6698
  const saveScheduler = useSaveScheduler(nostalgistRef);
6265
- const saveState = React5.useCallback(async () => {
6699
+ const saveState = React2.useCallback(async () => {
6266
6700
  if (!nostalgistRef.current) return null;
6267
6701
  const result = await saveScheduler.queueRewindCapture();
6268
6702
  return result?.data ?? null;
6269
6703
  }, [saveScheduler, nostalgistRef]);
6270
- const saveStateWithBlob = React5.useCallback(async () => {
6704
+ const saveStateWithBlob = React2.useCallback(async () => {
6271
6705
  if (!nostalgistRef.current) return null;
6272
6706
  return saveScheduler.save();
6273
6707
  }, [saveScheduler, nostalgistRef]);
6274
- const loadState = React5.useCallback(async (state) => {
6708
+ const loadState = React2.useCallback(async (state) => {
6275
6709
  if (!nostalgistRef.current) {
6276
6710
  console.warn("[Nostalgist] Cannot load state: emulator not running");
6277
6711
  return false;
@@ -6292,7 +6726,7 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
6292
6726
  return false;
6293
6727
  }
6294
6728
  }, [nostalgistRef, setIsPaused, setStatus]);
6295
- const startRewindCapture = React5.useCallback(() => {
6729
+ const startRewindCapture = React2.useCallback(() => {
6296
6730
  if (!rewindEnabled) {
6297
6731
  return;
6298
6732
  }
@@ -6320,13 +6754,13 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
6320
6754
  }
6321
6755
  }, 500);
6322
6756
  }, [isPaused, saveState, nostalgistRef]);
6323
- const stopRewindCapture = React5.useCallback(() => {
6757
+ const stopRewindCapture = React2.useCallback(() => {
6324
6758
  if (rewindCaptureIntervalRef.current) {
6325
6759
  clearInterval(rewindCaptureIntervalRef.current);
6326
6760
  rewindCaptureIntervalRef.current = null;
6327
6761
  }
6328
6762
  }, []);
6329
- const stopRewind = React5.useCallback(() => {
6763
+ const stopRewind = React2.useCallback(() => {
6330
6764
  if (!isRewinding) {
6331
6765
  return;
6332
6766
  }
@@ -6347,7 +6781,7 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
6347
6781
  console.error("[Nostalgist] Stop rewind error:", err);
6348
6782
  }
6349
6783
  }, [isRewinding, startRewindCapture, nostalgistRef]);
6350
- const startRewind = React5.useCallback(() => {
6784
+ const startRewind = React2.useCallback(() => {
6351
6785
  if (!nostalgistRef.current || isRewinding) {
6352
6786
  return;
6353
6787
  }
@@ -6386,7 +6820,7 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
6386
6820
  startRewindCapture();
6387
6821
  }
6388
6822
  }, [isRewinding, loadState, stopRewindCapture, startRewindCapture, stopRewind, nostalgistRef]);
6389
- React5.useEffect(() => {
6823
+ React2.useEffect(() => {
6390
6824
  return () => {
6391
6825
  saveScheduler.clearQueue();
6392
6826
  if (rewindIntervalRef.current) {
@@ -6414,7 +6848,7 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
6414
6848
  };
6415
6849
  }
6416
6850
  function useEmulatorCheats({ nostalgistRef }) {
6417
- const applyCheat = React5.useCallback((code) => {
6851
+ const applyCheat = React2.useCallback((code) => {
6418
6852
  if (!nostalgistRef.current) return;
6419
6853
  try {
6420
6854
  nostalgistRef.current.addCheat(code);
@@ -6422,7 +6856,7 @@ function useEmulatorCheats({ nostalgistRef }) {
6422
6856
  console.error("[Nostalgist] Apply cheat error:", err);
6423
6857
  }
6424
6858
  }, [nostalgistRef]);
6425
- const resetCheats = React5.useCallback(() => {
6859
+ const resetCheats = React2.useCallback(() => {
6426
6860
  if (!nostalgistRef.current) return;
6427
6861
  try {
6428
6862
  nostalgistRef.current.resetCheats();
@@ -6454,7 +6888,7 @@ var useNostalgist = ({
6454
6888
  shader,
6455
6889
  romId
6456
6890
  }) => {
6457
- const isHeavySystem = React5.useMemo(() => {
6891
+ const isHeavySystem = React2.useMemo(() => {
6458
6892
  return PERFORMANCE_TIER_2_SYSTEMS.has(system.toUpperCase());
6459
6893
  }, [system]);
6460
6894
  const {
@@ -6532,7 +6966,7 @@ var useNostalgist = ({
6532
6966
  } = useEmulatorCheats({
6533
6967
  nostalgistRef
6534
6968
  });
6535
- const start = React5.useCallback(async () => {
6969
+ const start = React2.useCallback(async () => {
6536
6970
  await coreStart();
6537
6971
  setTimeout(() => {
6538
6972
  if (nostalgistRef.current) {
@@ -6540,11 +6974,11 @@ var useNostalgist = ({
6540
6974
  }
6541
6975
  }, 2e3);
6542
6976
  }, [coreStart, startRewindCapture, nostalgistRef]);
6543
- const stop = React5.useCallback(() => {
6977
+ const stop = React2.useCallback(() => {
6544
6978
  stopRewindCapture();
6545
6979
  coreStop();
6546
6980
  }, [stopRewindCapture, coreStop]);
6547
- const hookReturn = React5.useMemo(() => ({
6981
+ const hookReturn = React2.useMemo(() => ({
6548
6982
  status,
6549
6983
  error,
6550
6984
  isPaused,
@@ -6615,18 +7049,18 @@ function useVolume({
6615
7049
  setVolume: setVolumeInHook,
6616
7050
  toggleMute: toggleMuteInHook
6617
7051
  }) {
6618
- const [volume, setVolumeState] = React5.useState(() => loadVolume());
6619
- const [isMuted, setIsMutedState] = React5.useState(() => loadMuteState());
6620
- React5.useEffect(() => {
7052
+ const [volume, setVolumeState] = React2.useState(() => loadVolume());
7053
+ const [isMuted, setIsMutedState] = React2.useState(() => loadMuteState());
7054
+ React2.useEffect(() => {
6621
7055
  setVolumeInHook(volume);
6622
7056
  }, [setVolumeInHook]);
6623
- const setVolume = React5.useCallback((newVolume) => {
7057
+ const setVolume = React2.useCallback((newVolume) => {
6624
7058
  const clampedVolume = Math.max(0, Math.min(100, newVolume));
6625
7059
  setVolumeState(clampedVolume);
6626
7060
  saveVolume(clampedVolume);
6627
7061
  setVolumeInHook(clampedVolume);
6628
7062
  }, [setVolumeInHook]);
6629
- const toggleMute = React5.useCallback(() => {
7063
+ const toggleMute = React2.useCallback(() => {
6630
7064
  setIsMutedState((prev) => {
6631
7065
  const newMuted = !prev;
6632
7066
  saveMuteState(newMuted);
@@ -6642,27 +7076,28 @@ function useVolume({
6642
7076
  };
6643
7077
  }
6644
7078
  function useControls(system, onNotify) {
7079
+ const t = useKoinTranslation();
6645
7080
  const defaultControls = getConsoleKeyboardDefaults(system || "SNES");
6646
- const [controls, setControls] = React5.useState(() => {
7081
+ const [controls, setControls] = React2.useState(() => {
6647
7082
  if (typeof window !== "undefined") {
6648
7083
  return loadKeyboardMapping(system);
6649
7084
  }
6650
7085
  return defaultControls;
6651
7086
  });
6652
- React5.useEffect(() => {
7087
+ React2.useEffect(() => {
6653
7088
  const loaded = loadKeyboardMapping(system);
6654
7089
  setControls(loaded);
6655
7090
  }, [system]);
6656
- const saveControls = React5.useCallback((newControls) => {
7091
+ const saveControls = React2.useCallback((newControls) => {
6657
7092
  setControls(newControls);
6658
7093
  saveKeyboardMapping(newControls, system);
6659
- onNotify?.("Controls saved", "success");
6660
- }, [system, onNotify]);
6661
- const resetToDefaults = React5.useCallback(() => {
7094
+ onNotify?.(t.notifications.controlsSaved, "success");
7095
+ }, [system, onNotify, t]);
7096
+ const resetToDefaults = React2.useCallback(() => {
6662
7097
  setControls(defaultControls);
6663
7098
  saveKeyboardMapping(defaultControls, system);
6664
- onNotify?.("Controls reset to defaults", "info");
6665
- }, [defaultControls, system, onNotify]);
7099
+ onNotify?.(t.notifications.controlsReset, "info");
7100
+ }, [defaultControls, system, onNotify, t]);
6666
7101
  return {
6667
7102
  controls,
6668
7103
  saveControls,
@@ -6688,16 +7123,17 @@ function useGameSession(props) {
6688
7123
  canvasRef,
6689
7124
  showToast
6690
7125
  } = props;
7126
+ const t = useKoinTranslation();
6691
7127
  const { controls, saveControls } = useControls(system, showToast);
6692
- const [gamepadModalOpen, setGamepadModalOpen] = React5.useState(false);
6693
- const [controlsModalOpen, setControlsModalOpen] = React5.useState(false);
7128
+ const [gamepadModalOpen, setGamepadModalOpen] = React2.useState(false);
7129
+ const [controlsModalOpen, setControlsModalOpen] = React2.useState(false);
6694
7130
  const { gamepads, connectedCount } = useGamepad({
6695
7131
  onConnect: (gamepad) => {
6696
7132
  showToast(
6697
- gamepad.name || "Controller ready to use",
7133
+ gamepad.name || t.notifications.controllerReady,
6698
7134
  "gamepad",
6699
7135
  {
6700
- title: "Controller Connected",
7136
+ title: t.notifications.controllerConnected,
6701
7137
  duration: 4e3,
6702
7138
  action: {
6703
7139
  label: "Configure",
@@ -6708,16 +7144,17 @@ function useGameSession(props) {
6708
7144
  },
6709
7145
  onDisconnect: () => {
6710
7146
  showToast(
6711
- "Controller was disconnected",
7147
+ t.notifications.controllerDisconnected,
6712
7148
  "warning",
6713
7149
  {
6714
- title: "Controller Lost",
7150
+ title: t.notifications.controllerDisconnected,
7151
+ // Title repeats or generic? Using same for now
6715
7152
  duration: 3e3
6716
7153
  }
6717
7154
  );
6718
7155
  }
6719
7156
  });
6720
- const gamepadBindings = React5.useMemo(() => {
7157
+ const gamepadBindings = React2.useMemo(() => {
6721
7158
  return [];
6722
7159
  }, [gamepads.length]);
6723
7160
  const nostalgist = useNostalgist({
@@ -6741,10 +7178,10 @@ function useGameSession(props) {
6741
7178
  if (arcadeSystems.includes(system.toLowerCase())) {
6742
7179
  setTimeout(() => {
6743
7180
  showToast(
6744
- "Press SHIFT to insert coin",
7181
+ t.notifications.insertCoin,
6745
7182
  "info",
6746
7183
  {
6747
- title: "\u{1FA99} Insert Coin",
7184
+ title: t.notifications.insertCoinTitle,
6748
7185
  duration: 5e3
6749
7186
  }
6750
7187
  );
@@ -6762,7 +7199,7 @@ function useGameSession(props) {
6762
7199
  toggleMute: toggleMuteInHook,
6763
7200
  prepare
6764
7201
  } = nostalgist;
6765
- React5.useEffect(() => {
7202
+ React2.useEffect(() => {
6766
7203
  return () => {
6767
7204
  if (status === "running" || status === "paused") {
6768
7205
  onSessionEnd?.();
@@ -6773,8 +7210,8 @@ function useGameSession(props) {
6773
7210
  setVolume: setVolumeInHook,
6774
7211
  toggleMute: toggleMuteInHook
6775
7212
  });
6776
- React5.useEffect(() => suppressEmulatorWarnings(), []);
6777
- React5.useEffect(() => {
7213
+ React2.useEffect(() => suppressEmulatorWarnings(), []);
7214
+ React2.useEffect(() => {
6778
7215
  if (!romUrl || !system || status !== "idle") return;
6779
7216
  const checkAndPrepare = async () => {
6780
7217
  if (canvasRef.current && canvasRef.current.isConnected) {
@@ -6786,7 +7223,7 @@ function useGameSession(props) {
6786
7223
  const rafId = requestAnimationFrame(checkAndPrepare);
6787
7224
  return () => cancelAnimationFrame(rafId);
6788
7225
  }, [romUrl, system, status, prepare]);
6789
- const hardcoreRestrictions = React5.useMemo(() => {
7226
+ const hardcoreRestrictions = React2.useMemo(() => {
6790
7227
  const isHardcore = !!retroAchievementsConfig?.hardcore;
6791
7228
  return {
6792
7229
  isHardcore,
@@ -6857,17 +7294,17 @@ function useAutoSave({
6857
7294
  queueRef,
6858
7295
  autoSaveInterval = 6e4
6859
7296
  }) {
6860
- const [autoSavePaused, setAutoSavePaused] = React5.useState(false);
6861
- const [autoSaveState, setAutoSaveState] = React5.useState("idle");
6862
- const [autoSaveProgress, setAutoSaveProgress] = React5.useState(0);
6863
- const onAutoSaveRef = React5.useRef(onAutoSave);
6864
- const nostalgistRef = React5.useRef(nostalgist);
6865
- React5.useEffect(() => {
7297
+ const [autoSavePaused, setAutoSavePaused] = React2.useState(false);
7298
+ const [autoSaveState, setAutoSaveState] = React2.useState("idle");
7299
+ const [autoSaveProgress, setAutoSaveProgress] = React2.useState(0);
7300
+ const onAutoSaveRef = React2.useRef(onAutoSave);
7301
+ const nostalgistRef = React2.useRef(nostalgist);
7302
+ React2.useEffect(() => {
6866
7303
  onAutoSaveRef.current = onAutoSave;
6867
7304
  nostalgistRef.current = nostalgist;
6868
7305
  }, [onAutoSave, nostalgist]);
6869
- const [loopTrigger, setLoopTrigger] = React5.useState(0);
6870
- React5.useEffect(() => {
7306
+ const [loopTrigger, setLoopTrigger] = React2.useState(0);
7307
+ React2.useEffect(() => {
6871
7308
  const currentNostalgist = nostalgist;
6872
7309
  if (!onAutoSave || !currentNostalgist || currentNostalgist.status !== "running" || autoSavePaused) {
6873
7310
  setAutoSaveState("idle");
@@ -6917,10 +7354,10 @@ function useAutoSave({
6917
7354
  clearTimeout(saveTimeoutId);
6918
7355
  };
6919
7356
  }, [nostalgist?.status, autoSavePaused, !!onAutoSave, loopTrigger, autoSaveInterval]);
6920
- const handleAutoSaveToggle = React5.useCallback(() => {
7357
+ const handleAutoSaveToggle = React2.useCallback(() => {
6921
7358
  setAutoSavePaused((prev) => !prev);
6922
7359
  }, []);
6923
- React5.useEffect(() => {
7360
+ React2.useEffect(() => {
6924
7361
  if (!onAutoSave || !nostalgist || nostalgist.status !== "running") return;
6925
7362
  const performEmergencySave = async () => {
6926
7363
  try {
@@ -6982,12 +7419,13 @@ function useGameSaves({
6982
7419
  onDeleteSaveState,
6983
7420
  autoSaveInterval
6984
7421
  }) {
6985
- const [saveModalOpen, setSaveModalOpen] = React5.useState(false);
6986
- const [saveModalMode, setSaveModalMode] = React5.useState("save");
6987
- const [saveSlots, setSaveSlots] = React5.useState([]);
6988
- const [isSlotLoading, setIsSlotLoading] = React5.useState(false);
6989
- const [actioningSlot, setActioningSlot] = React5.useState(null);
6990
- const queueRef = React5.useRef(new SaveQueue());
7422
+ const t = useKoinTranslation();
7423
+ const [saveModalOpen, setSaveModalOpen] = React2.useState(false);
7424
+ const [saveModalMode, setSaveModalMode] = React2.useState("save");
7425
+ const [saveSlots, setSaveSlots] = React2.useState([]);
7426
+ const [isSlotLoading, setIsSlotLoading] = React2.useState(false);
7427
+ const [actioningSlot, setActioningSlot] = React2.useState(null);
7428
+ const queueRef = React2.useRef(new SaveQueue());
6991
7429
  const {
6992
7430
  autoSaveEnabled,
6993
7431
  autoSavePaused,
@@ -7000,7 +7438,7 @@ function useGameSaves({
7000
7438
  queueRef,
7001
7439
  autoSaveInterval
7002
7440
  });
7003
- const refreshSlots = React5.useCallback(async () => {
7441
+ const refreshSlots = React2.useCallback(async () => {
7004
7442
  if (!onGetSaveSlots) return;
7005
7443
  setIsSlotLoading(true);
7006
7444
  try {
@@ -7008,7 +7446,7 @@ function useGameSaves({
7008
7446
  setSaveSlots(slots);
7009
7447
  } catch (err) {
7010
7448
  console.error("Failed to fetch save slots:", err);
7011
- showToast("Failed to load save slots", "error");
7449
+ showToast(t.notifications.failedFetch, "error", { title: t.overlays.toast.error });
7012
7450
  } finally {
7013
7451
  setIsSlotLoading(false);
7014
7452
  }
@@ -7025,7 +7463,7 @@ function useGameSaves({
7025
7463
  const result = await nostalgist.saveStateWithBlob();
7026
7464
  if (result) {
7027
7465
  await onSaveState(0, result.blob, void 0);
7028
- showToast("State saved successfully", "success");
7466
+ showToast(t.notifications.saved, "success", { title: t.overlays.toast.saved });
7029
7467
  }
7030
7468
  });
7031
7469
  } else {
@@ -7039,7 +7477,7 @@ function useGameSaves({
7039
7477
  a.download = `${fileName}.state`;
7040
7478
  a.click();
7041
7479
  URL.revokeObjectURL(url);
7042
- showToast("State downloaded", "success");
7480
+ showToast(t.notifications.downloaded, "success", { title: t.overlays.toast.saved });
7043
7481
  }
7044
7482
  });
7045
7483
  }
@@ -7058,9 +7496,9 @@ function useGameSaves({
7058
7496
  await queueRef.current.add(async () => {
7059
7497
  await nostalgist.loadState(new Uint8Array(buffer));
7060
7498
  });
7061
- showToast("State loaded successfully", "success");
7499
+ showToast(t.notifications.loaded, "success", { title: t.overlays.toast.loaded });
7062
7500
  } else {
7063
- showToast("No save found", "error");
7501
+ showToast(t.notifications.noSaveFound, "error", { title: t.overlays.toast.error });
7064
7502
  }
7065
7503
  } else {
7066
7504
  const input = document.createElement("input");
@@ -7073,7 +7511,7 @@ function useGameSaves({
7073
7511
  await queueRef.current.add(async () => {
7074
7512
  await nostalgist.loadState(new Uint8Array(buffer));
7075
7513
  });
7076
- showToast("State loaded from file", "success");
7514
+ showToast(t.notifications.loadedFile, "success", { title: t.overlays.toast.loaded });
7077
7515
  }
7078
7516
  };
7079
7517
  input.click();
@@ -7098,14 +7536,14 @@ function useGameSaves({
7098
7536
  console.warn("Screenshot failed", e);
7099
7537
  }
7100
7538
  await onSaveState(slot, result.blob, screen);
7101
- showToast(`Saved to slot ${slot}`, "success");
7539
+ showToast(t.notifications.savedSlot.replace("{{num}}", slot.toString()), "success", { title: t.overlays.toast.saved });
7102
7540
  setSaveModalOpen(false);
7103
7541
  resume();
7104
7542
  }
7105
7543
  });
7106
7544
  } catch (err) {
7107
7545
  console.error("Save failed:", err);
7108
- showToast("Failed to save", "error");
7546
+ showToast(t.notifications.failedSave, "error", { title: t.overlays.toast.error });
7109
7547
  } finally {
7110
7548
  setActioningSlot(null);
7111
7549
  }
@@ -7119,15 +7557,15 @@ function useGameSaves({
7119
7557
  await queueRef.current.add(async () => {
7120
7558
  await nostalgist.loadState(new Uint8Array(buffer));
7121
7559
  });
7122
- showToast(`Loaded from slot ${slot}`, "success");
7560
+ showToast(t.notifications.loadedSlot.replace("{{num}}", slot.toString()), "success", { title: t.overlays.toast.loaded });
7123
7561
  setSaveModalOpen(false);
7124
7562
  resume();
7125
7563
  } else {
7126
- showToast("Empty slot", "error");
7564
+ showToast(t.notifications.emptySlot, "error", { title: t.overlays.toast.error });
7127
7565
  }
7128
7566
  } catch (err) {
7129
7567
  console.error("Load failed:", err);
7130
- showToast("Failed to load", "error");
7568
+ showToast(t.notifications.failedLoad, "error", { title: t.overlays.toast.error });
7131
7569
  } finally {
7132
7570
  setActioningSlot(null);
7133
7571
  }
@@ -7139,11 +7577,11 @@ function useGameSaves({
7139
7577
  setActioningSlot(slot);
7140
7578
  try {
7141
7579
  await onDeleteSaveState(slot);
7142
- showToast(`Deleted slot ${slot}`, "success");
7580
+ showToast(t.notifications.deletedSlot.replace("{{num}}", slot.toString()), "success", { title: t.overlays.toast.saved });
7143
7581
  refreshSlots();
7144
7582
  } catch (err) {
7145
7583
  console.error("Delete failed:", err);
7146
- showToast("Failed to delete", "error");
7584
+ showToast(t.notifications.failedDelete, "error", { title: t.overlays.toast.error });
7147
7585
  } finally {
7148
7586
  setActioningSlot(null);
7149
7587
  }
@@ -7172,8 +7610,8 @@ function useGameCheats({
7172
7610
  cheats = [],
7173
7611
  onToggleCheat
7174
7612
  }) {
7175
- const [cheatsModalOpen, setCheatsModalOpen] = React5.useState(false);
7176
- const [activeCheats, setActiveCheats] = React5.useState(/* @__PURE__ */ new Set());
7613
+ const [cheatsModalOpen, setCheatsModalOpen] = React2.useState(false);
7614
+ const [activeCheats, setActiveCheats] = React2.useState(/* @__PURE__ */ new Set());
7177
7615
  const handleToggleCheat = (cheatId) => {
7178
7616
  if (!nostalgist) return;
7179
7617
  const newActiveCheats = new Set(activeCheats);
@@ -7205,16 +7643,16 @@ function useGameCheats({
7205
7643
  function useGameRecording({
7206
7644
  getCanvasElement
7207
7645
  }) {
7208
- const [isRecording, setIsRecording] = React5.useState(false);
7209
- const [isPaused, setIsPaused] = React5.useState(false);
7210
- const [recordingDuration, setRecordingDuration] = React5.useState(0);
7211
- const mediaRecorderRef = React5.useRef(null);
7212
- const chunksRef = React5.useRef([]);
7213
- const startTimeRef = React5.useRef(0);
7214
- const pausedTimeRef = React5.useRef(0);
7215
- const durationIntervalRef = React5.useRef(null);
7646
+ const [isRecording, setIsRecording] = React2.useState(false);
7647
+ const [isPaused, setIsPaused] = React2.useState(false);
7648
+ const [recordingDuration, setRecordingDuration] = React2.useState(0);
7649
+ const mediaRecorderRef = React2.useRef(null);
7650
+ const chunksRef = React2.useRef([]);
7651
+ const startTimeRef = React2.useRef(0);
7652
+ const pausedTimeRef = React2.useRef(0);
7653
+ const durationIntervalRef = React2.useRef(null);
7216
7654
  const isSupported = typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported("video/webm");
7217
- const startRecording = React5.useCallback(() => {
7655
+ const startRecording = React2.useCallback(() => {
7218
7656
  const canvas = getCanvasElement();
7219
7657
  if (!canvas || !isSupported) {
7220
7658
  console.warn("[Recording] Canvas not found or MediaRecorder not supported");
@@ -7252,7 +7690,7 @@ function useGameRecording({
7252
7690
  console.error("[Recording] Failed to start:", err);
7253
7691
  }
7254
7692
  }, [getCanvasElement, isSupported, isPaused]);
7255
- const stopRecording = React5.useCallback(async () => {
7693
+ const stopRecording = React2.useCallback(async () => {
7256
7694
  return new Promise((resolve) => {
7257
7695
  const mediaRecorder = mediaRecorderRef.current;
7258
7696
  if (!mediaRecorder || mediaRecorder.state === "inactive") {
@@ -7274,7 +7712,7 @@ function useGameRecording({
7274
7712
  mediaRecorder.stop();
7275
7713
  });
7276
7714
  }, []);
7277
- const pauseRecording = React5.useCallback(() => {
7715
+ const pauseRecording = React2.useCallback(() => {
7278
7716
  const mediaRecorder = mediaRecorderRef.current;
7279
7717
  if (mediaRecorder && mediaRecorder.state === "recording") {
7280
7718
  mediaRecorder.pause();
@@ -7283,7 +7721,7 @@ function useGameRecording({
7283
7721
  console.log("[Recording] Paused");
7284
7722
  }
7285
7723
  }, []);
7286
- const resumeRecording = React5.useCallback(() => {
7724
+ const resumeRecording = React2.useCallback(() => {
7287
7725
  const mediaRecorder = mediaRecorderRef.current;
7288
7726
  if (mediaRecorder && mediaRecorder.state === "paused") {
7289
7727
  pausedTimeRef.current = Date.now() - pausedTimeRef.current;
@@ -7292,7 +7730,7 @@ function useGameRecording({
7292
7730
  console.log("[Recording] Resumed");
7293
7731
  }
7294
7732
  }, []);
7295
- React5.useEffect(() => {
7733
+ React2.useEffect(() => {
7296
7734
  return () => {
7297
7735
  if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
7298
7736
  mediaRecorderRef.current.stop();
@@ -7466,9 +7904,9 @@ var DEFAULT_SETTINGS = {
7466
7904
  showInputDisplay: false
7467
7905
  };
7468
7906
  function usePlayerPersistence(onSettingsChange) {
7469
- const [settings, setSettings] = React5.useState(DEFAULT_SETTINGS);
7470
- const [isLoaded, setIsLoaded] = React5.useState(false);
7471
- React5.useEffect(() => {
7907
+ const [settings, setSettings] = React2.useState(DEFAULT_SETTINGS);
7908
+ const [isLoaded, setIsLoaded] = React2.useState(false);
7909
+ React2.useEffect(() => {
7472
7910
  try {
7473
7911
  const stored = localStorage.getItem(STORAGE_KEY2);
7474
7912
  if (stored) {
@@ -7480,7 +7918,7 @@ function usePlayerPersistence(onSettingsChange) {
7480
7918
  }
7481
7919
  setIsLoaded(true);
7482
7920
  }, []);
7483
- const updateSettings = React5.useCallback((updates) => {
7921
+ const updateSettings = React2.useCallback((updates) => {
7484
7922
  setSettings((prev) => {
7485
7923
  const next = { ...prev, ...updates };
7486
7924
  try {
@@ -7521,17 +7959,496 @@ var sendTelemetry = (eventName, params = {}) => {
7521
7959
  } catch (e) {
7522
7960
  }
7523
7961
  };
7524
- var GamePlayer = React5.memo(function GamePlayer2(props) {
7962
+
7963
+ // src/locales/es.ts
7964
+ var es = {
7965
+ controls: {
7966
+ play: "Jugar",
7967
+ pause: "Pausar",
7968
+ reset: "Reiniciar",
7969
+ rewind: "Rebobinar",
7970
+ save: "Guardar",
7971
+ load: "Cargar",
7972
+ snap: "Capturar",
7973
+ rec: "Grabar",
7974
+ stopRec: "Detener",
7975
+ startRecord: "Comenzar grabaci\xF3n",
7976
+ stopRecord: "Detener grabaci\xF3n",
7977
+ mute: "Silenciar",
7978
+ unmute: "Activar sonido",
7979
+ help: "Ayuda",
7980
+ full: "Pantalla comp.",
7981
+ // Abbreviated
7982
+ keys: "Teclas",
7983
+ menuOpen: "Abrir men\xFA",
7984
+ menuClose: "Cerrar men\xFA",
7985
+ gamepadConnected: "{{count}} mando{{plural}} conectado(s) - haz clic para configurar",
7986
+ noGamepad: "No hay mando detectado - presiona cualquier bot\xF3n para conectar",
7987
+ press: "Pulsa",
7988
+ startBtn: "START",
7989
+ selectBtn: "SELECT"
7990
+ },
7991
+ common: {
7992
+ disabledInHardcore: "Desactivado en modo Hardcore",
7993
+ notSupported: "No compatible con esta consola",
7994
+ playToEnableRewind: "Juega unos segundos para activar el rebobinado"
7995
+ },
7996
+ settings: {
7997
+ title: "Ajustes",
7998
+ general: "General",
7999
+ audio: "Sonido",
8000
+ video: "V\xEDdeo",
8001
+ input: "Entrada",
8002
+ advanced: "Avanzado",
8003
+ fullscreen: "Pantalla completa",
8004
+ controls: "Controles",
8005
+ gamepad: "Mando",
8006
+ cheats: "Trucos",
8007
+ retroAchievements: "RetroAchievements",
8008
+ shortcuts: "Atajos",
8009
+ exit: "Salir",
8010
+ language: "Idioma",
8011
+ selectLanguage: "Seleccionar idioma"
8012
+ },
8013
+ overlay: {
8014
+ play: "JUGAR",
8015
+ systemFirmware: "FIRMWARE DEL SISTEMA",
8016
+ loading: "Cargando {{system}}",
8017
+ initializing: "Inicializando emulador",
8018
+ loadingSave: "Cargando partida",
8019
+ preparingSlot: "Preparando ranura {{num}}",
8020
+ systemError: "Error del sistema",
8021
+ failedInit: "Error al inicializar el emulador",
8022
+ retry: "Reintentar",
8023
+ slotReady: "Ranura {{num}} lista",
8024
+ paused: "Pausado"
8025
+ },
8026
+ notifications: {
8027
+ saved: "Estado guardado",
8028
+ loaded: "Estado cargado",
8029
+ error: "Error",
8030
+ recordingStarted: "Grabaci\xF3n iniciada",
8031
+ recordingSaved: "Grabaci\xF3n guardada",
8032
+ downloaded: "Estado descargado",
8033
+ loadedFile: "Estado cargado desde archivo",
8034
+ savedSlot: "Guardado en ranura {{num}}",
8035
+ loadedSlot: "Cargado desde ranura {{num}}",
8036
+ deletedSlot: "Ranura {{num}} eliminada",
8037
+ emptySlot: "Ranura vac\xEDa",
8038
+ noSaveFound: "No se encontr\xF3 partida guardada",
8039
+ failedSave: "Error al guardar",
8040
+ failedLoad: "Error al cargar",
8041
+ failedDelete: "Error al eliminar",
8042
+ failedFetch: "Error al cargar ranuras",
8043
+ controllerConnected: "Mando conectado",
8044
+ controllerDisconnected: "Mando desconectado",
8045
+ controllerReady: "Mando listo para usar",
8046
+ insertCoin: "Pulsa SHIFT para insertar moneda",
8047
+ insertCoinTitle: "\u{1FA99} Insertar Moneda",
8048
+ controlsSaved: "Controles guardados",
8049
+ controlsReset: "Controles restablecidos"
8050
+ },
8051
+ modals: {
8052
+ shortcuts: {
8053
+ title: "Atajos de teclado",
8054
+ playerShortcuts: "Atajos del reproductor",
8055
+ overlays: "Superposiciones",
8056
+ recording: "Grabaci\xF3n",
8057
+ showHelp: "Mostrar ayuda",
8058
+ perfOverlay: "Superposici\xF3n de rendimiento",
8059
+ inputDisplay: "Mostrar entrada",
8060
+ toggleRec: "Alternar grabaci\xF3n",
8061
+ toggleMute: "Alternar silencio",
8062
+ pressEsc: "Pulsa ESC para cerrar"
8063
+ },
8064
+ controls: {
8065
+ title: "Controles",
8066
+ keyboard: "Asignaci\xF3n de teclado",
8067
+ description: "Haz clic en un bot\xF3n y pulsa una tecla",
8068
+ pressKey: "Pulsa una tecla...",
8069
+ reset: "Restablecer",
8070
+ save: "Guardar controles"
8071
+ },
8072
+ gamepad: {
8073
+ title: "Ajustes de mando",
8074
+ noGamepad: "No se detecta mando",
8075
+ connected: "{{count}} mando{{s}} conectado(s)",
8076
+ none: "Ning\xFAn mando detectado",
8077
+ player: "Jugador:",
8078
+ noController: "Sin mando",
8079
+ pressAny: "Pulsa cualquier bot\xF3n para conectar",
8080
+ waiting: "Esperando entrada...",
8081
+ pressButton: "Pulsa bot\xF3n para {{button}}",
8082
+ pressEsc: "Pulsa Escape para cancelar",
8083
+ reset: "Restablecer",
8084
+ save: "Guardar ajustes"
8085
+ },
8086
+ cheats: {
8087
+ title: "Trucos",
8088
+ addCheat: "A\xF1adir truco",
8089
+ available: "{{count}} truco{{s}} disponible(s)",
8090
+ emptyTitle: "No hay trucos disponibles",
8091
+ emptyDesc: "No se encontraron c\xF3digos para este juego",
8092
+ copy: "Copiar c\xF3digo",
8093
+ active: "{{count}} truco{{s}} activo(s)",
8094
+ toggleHint: "Haz clic para activar/desactivar"
8095
+ },
8096
+ saveSlots: {
8097
+ title: "Guardar partida",
8098
+ saveTitle: "Guardar",
8099
+ loadTitle: "Cargar",
8100
+ emptySlot: "Ranura vac\xEDa",
8101
+ subtitleSave: "Elige una ranura para guardar",
8102
+ subtitleLoad: "Elige una ranura para cargar",
8103
+ loading: "Cargando partidas...",
8104
+ locked: "Ranura {{num}} bloqueada",
8105
+ upgrade: "Mejora para desbloquear m\xE1s ranuras",
8106
+ autoSave: "Autoguardado",
8107
+ autoSaveDesc: "Reservado para guardado autom\xE1tico",
8108
+ noData: "Sin datos",
8109
+ slot: "Ranura {{num}}",
8110
+ footerSave: "Las partidas se guardan en la nube y se sincronizan",
8111
+ footerLoad: "Tu progreso se restaurar\xE1 al punto seleccionado"
8112
+ },
8113
+ bios: {
8114
+ title: "Selecci\xF3n de BIOS",
8115
+ description: "Selecciona una BIOS para este juego",
8116
+ warningTitle: "Nota:",
8117
+ warning: "Cambiar la BIOS reiniciar\xE1 el emulador. Se perder\xE1 el progreso no guardado.",
8118
+ systemDefault: "Por defecto",
8119
+ active: "Activo",
8120
+ defaultDesc: "Usar BIOS interna del emulador",
8121
+ emptyTitle: "No se encontraron archivos de BIOS.",
8122
+ emptyDesc: "Sube archivos BIOS en tu biblioteca.",
8123
+ footer: "Firmware del sistema"
8124
+ }
8125
+ },
8126
+ retroAchievements: {
8127
+ title: "RetroAchievements",
8128
+ login: "Iniciar sesi\xF3n",
8129
+ logout: "Cerrar sesi\xF3n",
8130
+ username: "Usuario",
8131
+ password: "Password",
8132
+ hardcore: "Modo Hardcore",
8133
+ achievements: "Logros",
8134
+ locked: "Bloqueado",
8135
+ unlocked: "Desbloqueado",
8136
+ mastered: "Dominado",
8137
+ identifying: "Identificando juego...",
8138
+ achievementsAvailable: "{{count}} logros disponibles",
8139
+ gameNotSupported: "Conectado - Juego no en base de datos",
8140
+ connect: "Conectar RetroAchievements",
8141
+ connected: "RetroAchievements (conectado)",
8142
+ createAccount: "Crear cuenta",
8143
+ privacy: "Privacidad:",
8144
+ privacyText: "Tu contrase\xF1a solo se usa para autenticaci\xF3n con RA. No se almacena.",
8145
+ connecting: "Conectando...",
8146
+ connectAccount: "Conecta tu cuenta para rastrear logros",
8147
+ poweredBy: "Con la tecnolog\xEDa de",
8148
+ connectedStatus: "Conectado",
8149
+ yourUsername: "Tu usuario RA",
8150
+ yourPassword: "Tu contrase\xF1a RA",
8151
+ usernameRequired: "Usuario y contrase\xF1a requeridos",
8152
+ noGame: "Sin juego cargado",
8153
+ loadGame: "Carga un juego para ver los logros",
8154
+ noAchievements: "No se encontraron logros",
8155
+ notSupported: "Este juego puede no ser compatible",
8156
+ ptsRemaining: "{{count}} pts restantes",
8157
+ filters: {
8158
+ all: "Todos",
8159
+ locked: "Bloqueado",
8160
+ unlocked: "Desbloqueado"
8161
+ }
8162
+ },
8163
+ overlays: {
8164
+ performance: {
8165
+ title: "Estad\xEDsticas",
8166
+ fps: "FPS",
8167
+ frameTime: "FT",
8168
+ memory: "MEM",
8169
+ core: "Core",
8170
+ input: "ENTRADA",
8171
+ active: "ACTIVO"
8172
+ },
8173
+ toast: {
8174
+ saved: "Juego guardado",
8175
+ loaded: "Juego cargado",
8176
+ error: "Error"
8177
+ },
8178
+ recording: {
8179
+ started: "Grabaci\xF3n iniciada",
8180
+ stopped: "Grabaci\xF3n detenida",
8181
+ saved: "Grabaci\xF3n guardada",
8182
+ paused: "PAUSA",
8183
+ recording: "GRABANDO",
8184
+ resume: "Reanudar grabaci\xF3n",
8185
+ pause: "Pausar grabaci\xF3n",
8186
+ stop: "Detener y guardar",
8187
+ hover: "Controles"
8188
+ }
8189
+ }
8190
+ };
8191
+
8192
+ // src/locales/fr.ts
8193
+ var fr = {
8194
+ controls: {
8195
+ play: "Jouer",
8196
+ pause: "Pause",
8197
+ reset: "R\xE9initialiser",
8198
+ rewind: "Rembobiner",
8199
+ save: "Sauver",
8200
+ load: "Charger",
8201
+ snap: "Photo",
8202
+ rec: "Enr.",
8203
+ stopRec: "Arr\xEAter",
8204
+ startRecord: "D\xE9marrer l'enregistrement",
8205
+ stopRecord: "Arr\xEAter l'enregistrement",
8206
+ mute: "Muet",
8207
+ unmute: "Son",
8208
+ help: "Aide",
8209
+ full: "Plein \xE9cran",
8210
+ keys: "Touches",
8211
+ menuOpen: "Ouvrir menu",
8212
+ menuClose: "Fermer menu",
8213
+ gamepadConnected: "{{count}} manette{{plural}} connect\xE9e(s)",
8214
+ noGamepad: "Aucune manette d\xE9tect\xE9e",
8215
+ press: "Appuyez",
8216
+ startBtn: "START",
8217
+ selectBtn: "SELECT"
8218
+ },
8219
+ common: {
8220
+ disabledInHardcore: "D\xE9sactiv\xE9 en mode Hardcore",
8221
+ notSupported: "Non support\xE9 sur cette console",
8222
+ playToEnableRewind: "Jouez quelques secondes pour activer le rembobinage"
8223
+ },
8224
+ settings: {
8225
+ title: "Param\xE8tres",
8226
+ general: "G\xE9n\xE9ral",
8227
+ audio: "Audio",
8228
+ video: "Vid\xE9o",
8229
+ input: "Entr\xE9e",
8230
+ advanced: "Avanc\xE9",
8231
+ fullscreen: "Plein \xE9cran",
8232
+ controls: "Contr\xF4les",
8233
+ gamepad: "Manette",
8234
+ cheats: "Codes",
8235
+ retroAchievements: "Succ\xE8s",
8236
+ shortcuts: "Raccourcis",
8237
+ exit: "Quitter",
8238
+ language: "Langue",
8239
+ selectLanguage: "Choisir la langue"
8240
+ },
8241
+ overlay: {
8242
+ play: "JOUER",
8243
+ systemFirmware: "MICROLOGICIEL SYST\xC8ME",
8244
+ loading: "Chargement {{system}}",
8245
+ initializing: "Initialisation de l'\xE9mulateur",
8246
+ loadingSave: "Chargement de la sauvegarde",
8247
+ preparingSlot: "Pr\xE9paration de l'emplacement {{num}}",
8248
+ systemError: "Erreur syst\xE8me",
8249
+ failedInit: "\xC9chec de l'initialisation",
8250
+ retry: "R\xE9essayer",
8251
+ slotReady: "Emplacement {{num}} pr\xEAt",
8252
+ paused: "Pause"
8253
+ },
8254
+ notifications: {
8255
+ saved: "\xC9tat sauvegard\xE9",
8256
+ loaded: "\xC9tat charg\xE9",
8257
+ error: "Erreur",
8258
+ recordingStarted: "Enregistrement d\xE9marr\xE9",
8259
+ recordingSaved: "Enregistrement sauvegard\xE9",
8260
+ downloaded: "\xC9tat t\xE9l\xE9charg\xE9",
8261
+ loadedFile: "\xC9tat charg\xE9 depuis fichier",
8262
+ savedSlot: "Sauvegard\xE9 sur l'emplacement {{num}}",
8263
+ loadedSlot: "Charg\xE9 depuis l'emplacement {{num}}",
8264
+ deletedSlot: "Emplacement {{num}} supprim\xE9",
8265
+ emptySlot: "Emplacement vide",
8266
+ noSaveFound: "Aucune sauvegarde trouv\xE9e",
8267
+ failedSave: "\xC9chec de la sauvegarde",
8268
+ failedLoad: "\xC9chec du chargement",
8269
+ failedDelete: "\xC9chec de la suppression",
8270
+ failedFetch: "\xC9chec du chargement des emplacements",
8271
+ controllerConnected: "Manette connect\xE9e",
8272
+ controllerDisconnected: "Manette d\xE9connect\xE9e",
8273
+ controllerReady: "Manette pr\xEAte",
8274
+ insertCoin: "Appuyez sur SHIFT pour ins\xE9rer une pi\xE8ce",
8275
+ insertCoinTitle: "\u{1FA99} Ins\xE9rer Pi\xE8ce",
8276
+ controlsSaved: "Contr\xF4les sauvegard\xE9s",
8277
+ controlsReset: "Contr\xF4les r\xE9initialis\xE9s"
8278
+ },
8279
+ modals: {
8280
+ shortcuts: {
8281
+ title: "Raccourcis clavier",
8282
+ playerShortcuts: "Raccourcis du lecteur",
8283
+ overlays: "Superpositions",
8284
+ recording: "Enregistrement",
8285
+ showHelp: "Afficher l'aide",
8286
+ perfOverlay: "Overlay de performance",
8287
+ inputDisplay: "Afficher les entr\xE9es",
8288
+ toggleRec: "Basculer l'enregistrement",
8289
+ toggleMute: "Basculer le son",
8290
+ pressEsc: "Appuyez sur ESC pour fermer"
8291
+ },
8292
+ controls: {
8293
+ title: "Contr\xF4les",
8294
+ keyboard: "Configuration clavier",
8295
+ description: "Cliquez sur un bouton et appuyez sur une touche",
8296
+ pressKey: "Appuyez...",
8297
+ reset: "R\xE9initialiser",
8298
+ save: "Sauvegarder"
8299
+ },
8300
+ gamepad: {
8301
+ title: "Param\xE8tres manette",
8302
+ noGamepad: "Aucune manette",
8303
+ connected: "{{count}} manette{{s}} connect\xE9e(s)",
8304
+ none: "Aucune manette d\xE9tect\xE9e",
8305
+ player: "Joueur:",
8306
+ noController: "Aucune manette",
8307
+ pressAny: "Appuyez sur un bouton pour connecter",
8308
+ waiting: "En attente...",
8309
+ pressButton: "Appuyez pour {{button}}",
8310
+ pressEsc: "Echap pour annuler",
8311
+ reset: "R\xE9initialiser",
8312
+ save: "Sauvegarder"
8313
+ },
8314
+ cheats: {
8315
+ title: "Codes de triche",
8316
+ addCheat: "Ajouter un code",
8317
+ available: "{{count}} code{{s}} disponible(s)",
8318
+ emptyTitle: "Aucun code disponible",
8319
+ emptyDesc: "Aucun code trouv\xE9 pour ce jeu",
8320
+ copy: "Copier",
8321
+ active: "{{count}} actif(s)",
8322
+ toggleHint: "Cliquez pour activer/d\xE9sactiver"
8323
+ },
8324
+ saveSlots: {
8325
+ title: "Sauvegardes",
8326
+ saveTitle: "Sauvegarder",
8327
+ loadTitle: "Charger",
8328
+ emptySlot: "Vide",
8329
+ subtitleSave: "Choisir un emplacement",
8330
+ subtitleLoad: "Choisir une sauvegarde",
8331
+ loading: "Chargement...",
8332
+ locked: "Emplacement {{num}} verrouill\xE9",
8333
+ upgrade: "Am\xE9liorer pour plus d'emplacements",
8334
+ autoSave: "Auto-Sauvegarde",
8335
+ autoSaveDesc: "R\xE9serv\xE9 \xE0 la sauvegarde automatique",
8336
+ noData: "Aucune donn\xE9e",
8337
+ slot: "Emplacement {{num}}",
8338
+ footerSave: "Les sauvegardes sont synchronis\xE9es dans le cloud",
8339
+ footerLoad: "Votre progression sera restaur\xE9e"
8340
+ },
8341
+ bios: {
8342
+ title: "S\xE9lection BIOS",
8343
+ description: "S\xE9lectionnez un BIOS",
8344
+ warningTitle: "Note:",
8345
+ warning: "Changer le BIOS red\xE9marre l'\xE9mulateur. Progression non sauvegard\xE9e sera perdue.",
8346
+ systemDefault: "Par d\xE9faut",
8347
+ active: "Actif",
8348
+ defaultDesc: "Utiliser le BIOS par d\xE9faut",
8349
+ emptyTitle: "Aucun BIOS trouv\xE9.",
8350
+ emptyDesc: "T\xE9l\xE9versez des fichiers BIOS dans votre biblioth\xE8que.",
8351
+ footer: "Firmware syst\xE8me"
8352
+ }
8353
+ },
8354
+ retroAchievements: {
8355
+ title: "RetroAchievements",
8356
+ login: "Connexion",
8357
+ logout: "D\xE9connexion",
8358
+ username: "Utilisateur",
8359
+ password: "Mot de passe",
8360
+ hardcore: "Mode Hardcore",
8361
+ achievements: "Succ\xE8s",
8362
+ locked: "Verrouill\xE9",
8363
+ unlocked: "D\xE9verrouill\xE9",
8364
+ mastered: "Ma\xEEtris\xE9",
8365
+ identifying: "Identification...",
8366
+ achievementsAvailable: "{{count}} succ\xE8s disponibles",
8367
+ gameNotSupported: "Jeu non support\xE9 par RA",
8368
+ connect: "Connecter RetroAchievements",
8369
+ connected: "RetroAchievements (connect\xE9)",
8370
+ createAccount: "Cr\xE9er un compte",
8371
+ privacy: "Confidentialit\xE9:",
8372
+ privacyText: "Votre mot de passe n'est pas stock\xE9.",
8373
+ connecting: "Connexion...",
8374
+ connectAccount: "Connectez votre compte",
8375
+ poweredBy: "Propuls\xE9 par",
8376
+ connectedStatus: "Connect\xE9",
8377
+ yourUsername: "Votre utilisateur RA",
8378
+ yourPassword: "Votre mot de passe RA",
8379
+ usernameRequired: "Utilisateur et mot de passe requis",
8380
+ noGame: "Aucun jeu",
8381
+ loadGame: "Chargez un jeu pour voir les succ\xE8s",
8382
+ noAchievements: "Aucun succ\xE8s trouv\xE9",
8383
+ notSupported: "Ce jeu n'est peut-\xEAtre pas support\xE9",
8384
+ ptsRemaining: "{{count}} pts restants",
8385
+ filters: {
8386
+ all: "Tous",
8387
+ locked: "Verrouill\xE9s",
8388
+ unlocked: "D\xE9verrouill\xE9s"
8389
+ }
8390
+ },
8391
+ overlays: {
8392
+ performance: {
8393
+ title: "Stats",
8394
+ fps: "FPS",
8395
+ frameTime: "FT",
8396
+ memory: "MEM",
8397
+ core: "Core",
8398
+ input: "ENTR\xC9E",
8399
+ active: "ACTIF"
8400
+ },
8401
+ toast: {
8402
+ saved: "Sauvegard\xE9",
8403
+ loaded: "Charg\xE9",
8404
+ error: "Erreur"
8405
+ },
8406
+ recording: {
8407
+ started: "Enregistrement d\xE9marr\xE9",
8408
+ stopped: "Enregistrement arr\xEAt\xE9",
8409
+ saved: "Enregistrement sauvegard\xE9",
8410
+ paused: "PAUSE",
8411
+ recording: "ENR.",
8412
+ resume: "Reprendre",
8413
+ pause: "Pause",
8414
+ stop: "Arr\xEAter et sauver",
8415
+ hover: "Contr\xF4les"
8416
+ }
8417
+ }
8418
+ };
8419
+
8420
+ // src/lib/common-utils.ts
8421
+ function deepMerge2(target, source) {
8422
+ const isObject = (obj) => obj && typeof obj === "object";
8423
+ if (!isObject(target) || !isObject(source)) {
8424
+ return source;
8425
+ }
8426
+ const output = { ...target };
8427
+ Object.keys(source).forEach((key) => {
8428
+ const targetValue = output[key];
8429
+ const sourceValue = source[key];
8430
+ if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
8431
+ output[key] = sourceValue;
8432
+ } else if (isObject(targetValue) && isObject(sourceValue)) {
8433
+ output[key] = deepMerge2(targetValue, sourceValue);
8434
+ } else {
8435
+ output[key] = sourceValue;
8436
+ }
8437
+ });
8438
+ return output;
8439
+ }
8440
+ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
7525
8441
  const { settings, updateSettings, isLoaded: settingsLoaded } = usePlayerPersistence();
7526
- React5.useEffect(() => {
8442
+ React2.useEffect(() => {
7527
8443
  sendTelemetry("game_start", {
7528
8444
  system: props.system,
7529
8445
  core: props.core,
7530
8446
  game: props.title || "unknown"
7531
8447
  });
7532
8448
  }, [props.system, props.core, props.title]);
7533
- const [biosModalOpen, setBiosModalOpen] = React5.useState(false);
7534
- const [showShortcutsModal, setShowShortcutsModal] = React5.useState(false);
8449
+ const [biosModalOpen, setBiosModalOpen] = React2.useState(false);
8450
+ const [showShortcutsModal, setShowShortcutsModal] = React2.useState(false);
8451
+ const [settingsModalOpen, setSettingsModalOpen] = React2.useState(false);
7535
8452
  const effectiveShader = props.shader !== void 0 ? props.shader : settings.shader;
7536
8453
  const {
7537
8454
  // Refs
@@ -7627,72 +8544,76 @@ var GamePlayer = React5.memo(function GamePlayer2(props) {
7627
8544
  status,
7628
8545
  isPerformanceMode
7629
8546
  } = nostalgist;
7630
- React5.useEffect(() => {
8547
+ React2.useEffect(() => {
7631
8548
  if (status === "running") {
7632
8549
  console.log("[Koin Debug] Status:", status);
7633
8550
  console.log("[Koin Debug] isPerformanceMode:", isPerformanceMode);
7634
8551
  console.log("[Koin Debug] crossOriginIsolated:", typeof window !== "undefined" ? window.crossOriginIsolated : "N/A");
7635
8552
  }
7636
8553
  }, [status, isPerformanceMode]);
7637
- React5.useEffect(() => {
8554
+ React2.useEffect(() => {
7638
8555
  if (settingsLoaded) {
7639
8556
  setVolume(settings.volume);
7640
8557
  if (muted !== settings.muted) toggleMute();
7641
8558
  }
7642
8559
  }, [settingsLoaded]);
7643
8560
  const { system, systemColor = "#00FF41", cheats = [], onExit } = props;
7644
- const handlePauseToggle = React5.useCallback(() => {
8561
+ const handlePauseToggle = React2.useCallback(() => {
7645
8562
  status === "ready" ? start() : togglePause();
7646
8563
  }, [status, start, togglePause]);
7647
- const handleScreenshot = React5.useCallback(async () => {
8564
+ const handleScreenshot = React2.useCallback(async () => {
7648
8565
  const result = await screenshot();
7649
8566
  if (result && props.onScreenshotCaptured) {
7650
8567
  props.onScreenshotCaptured(result);
7651
8568
  }
7652
8569
  }, [screenshot, props.onScreenshotCaptured]);
7653
- const handleShowControls = React5.useCallback(() => {
8570
+ const handleShowControls = React2.useCallback(() => {
7654
8571
  pause();
7655
8572
  setControlsModalOpen(true);
7656
8573
  }, [pause, setControlsModalOpen]);
7657
- const handleShowCheats = React5.useCallback(() => {
8574
+ const handleShowCheats = React2.useCallback(() => {
7658
8575
  pause();
7659
8576
  setCheatsModalOpen(true);
7660
8577
  }, [pause, setCheatsModalOpen]);
7661
- const handleShowRA = React5.useCallback(() => {
8578
+ const handleShowRA = React2.useCallback(() => {
7662
8579
  pause();
7663
8580
  setRaSidebarOpen(true);
7664
8581
  }, [pause, setRaSidebarOpen]);
7665
- const handleShowGamepadSettings = React5.useCallback(() => {
8582
+ const handleShowGamepadSettings = React2.useCallback(() => {
7666
8583
  pause();
7667
8584
  setGamepadModalOpen(true);
7668
8585
  }, [pause, setGamepadModalOpen]);
7669
- const handleExitClick = React5.useCallback(() => {
8586
+ const handleShowSettings = React2.useCallback(() => {
8587
+ pause();
8588
+ setSettingsModalOpen(true);
8589
+ }, [pause, setSettingsModalOpen]);
8590
+ const handleExitClick = React2.useCallback(() => {
7670
8591
  onExit?.();
7671
8592
  }, [onExit]);
7672
- const handleBiosSelection = React5.useCallback(() => {
8593
+ const handleBiosSelection = React2.useCallback(() => {
7673
8594
  setBiosModalOpen(true);
7674
8595
  }, [setBiosModalOpen]);
7675
- const handleVolumeChange = React5.useCallback((val) => {
8596
+ const handleVolumeChange = React2.useCallback((val) => {
7676
8597
  setVolume(val);
7677
8598
  updateSettings({ volume: val });
7678
8599
  }, [setVolume, updateSettings]);
7679
- const handleToggleMute = React5.useCallback(() => {
8600
+ const handleToggleMute = React2.useCallback(() => {
7680
8601
  toggleMute();
7681
8602
  updateSettings({ muted: !muted });
7682
8603
  }, [toggleMute, updateSettings, muted]);
7683
- const handleShaderChange = React5.useCallback((newShader, requiresRestart) => {
8604
+ const handleShaderChange = React2.useCallback((newShader, requiresRestart) => {
7684
8605
  updateSettings({ shader: newShader });
7685
8606
  if (props.onShaderChange) {
7686
8607
  props.onShaderChange(newShader, requiresRestart);
7687
8608
  }
7688
8609
  }, [updateSettings, props.onShaderChange]);
7689
- const handleTogglePerformanceOverlay = React5.useCallback(() => {
8610
+ const handleTogglePerformanceOverlay = React2.useCallback(() => {
7690
8611
  updateSettings({ showPerformanceOverlay: !settings.showPerformanceOverlay });
7691
8612
  }, [updateSettings, settings.showPerformanceOverlay]);
7692
- const handleToggleInputDisplay = React5.useCallback(() => {
8613
+ const handleToggleInputDisplay = React2.useCallback(() => {
7693
8614
  updateSettings({ showInputDisplay: !settings.showInputDisplay });
7694
8615
  }, [updateSettings, settings.showInputDisplay]);
7695
- const handleToggleRecording = React5.useCallback(async () => {
8616
+ const handleToggleRecording = React2.useCallback(async () => {
7696
8617
  if (!recordingSupported) {
7697
8618
  console.warn("[Recording] Not supported in this browser");
7698
8619
  return;
@@ -7713,14 +8634,14 @@ var GamePlayer = React5.memo(function GamePlayer2(props) {
7713
8634
  startRecording();
7714
8635
  }
7715
8636
  }, [isRecording, recordingSupported, startRecording, stopRecording]);
7716
- const handleToggleShortcuts = React5.useCallback(() => {
8637
+ const handleToggleShortcuts = React2.useCallback(() => {
7717
8638
  setShowShortcutsModal((prev) => {
7718
8639
  if (!prev) pause();
7719
8640
  else resume();
7720
8641
  return !prev;
7721
8642
  });
7722
8643
  }, [pause, resume]);
7723
- React5.useEffect(() => {
8644
+ React2.useEffect(() => {
7724
8645
  const handleKeyDown = (e) => {
7725
8646
  if (e.key === "F1") {
7726
8647
  e.preventDefault();
@@ -7877,6 +8798,7 @@ var GamePlayer = React5.memo(function GamePlayer2(props) {
7877
8798
  systemColor,
7878
8799
  gamepadCount: connectedCount,
7879
8800
  onGamepadSettings: handleShowGamepadSettings,
8801
+ onSettings: handleShowSettings,
7880
8802
  volume,
7881
8803
  isMuted: muted,
7882
8804
  onVolumeChange: handleVolumeChange,
@@ -7942,7 +8864,11 @@ var GamePlayer = React5.memo(function GamePlayer2(props) {
7942
8864
  setBiosModalOpen,
7943
8865
  availableBios: props.availableBios,
7944
8866
  currentBiosId: props.currentBiosId,
7945
- onSelectBios: props.onSelectBios
8867
+ onSelectBios: props.onSelectBios,
8868
+ settingsModalOpen,
8869
+ setSettingsModalOpen,
8870
+ currentLanguage: props.currentLanguage,
8871
+ onLanguageChange: props.onLanguageChange
7946
8872
  }
7947
8873
  ),
7948
8874
  !isMobile && /* @__PURE__ */ jsxRuntime.jsx(
@@ -7970,6 +8896,27 @@ var GamePlayer = React5.memo(function GamePlayer2(props) {
7970
8896
  }
7971
8897
  ) });
7972
8898
  });
8899
+ var GamePlayer = React2.memo(function GamePlayer2(props) {
8900
+ const [currentLanguage, setCurrentLanguage] = React2.useState(props.initialLanguage || "en");
8901
+ const effectiveTranslations = React2.useMemo(() => {
8902
+ const base = currentLanguage === "es" ? es : currentLanguage === "fr" ? fr : en;
8903
+ if (props.translations) {
8904
+ return deepMerge2(base, props.translations);
8905
+ }
8906
+ return base;
8907
+ }, [currentLanguage, props.translations]);
8908
+ const handleLanguageChange = React2.useCallback((lang) => {
8909
+ setCurrentLanguage(lang);
8910
+ }, []);
8911
+ return /* @__PURE__ */ jsxRuntime.jsx(KoinI18nProvider, { translations: effectiveTranslations, children: /* @__PURE__ */ jsxRuntime.jsx(
8912
+ GamePlayerInner,
8913
+ {
8914
+ ...props,
8915
+ currentLanguage,
8916
+ onLanguageChange: handleLanguageChange
8917
+ }
8918
+ ) });
8919
+ });
7973
8920
  var GamePlayer_default = GamePlayer;
7974
8921
  function AchievementPopup({
7975
8922
  achievement,
@@ -7977,9 +8924,9 @@ function AchievementPopup({
7977
8924
  onDismiss,
7978
8925
  autoDismissMs = 5e3
7979
8926
  }) {
7980
- const [isVisible, setIsVisible] = React5.useState(false);
7981
- const [isExiting, setIsExiting] = React5.useState(false);
7982
- React5.useEffect(() => {
8927
+ const [isVisible, setIsVisible] = React2.useState(false);
8928
+ const [isExiting, setIsExiting] = React2.useState(false);
8929
+ React2.useEffect(() => {
7983
8930
  requestAnimationFrame(() => {
7984
8931
  setIsVisible(true);
7985
8932
  });
@@ -8056,15 +9003,15 @@ var SHADER_PRESETS = [
8056
9003
  { id: "handheld/lcd-grid-v2", name: "LCD Grid", description: "Game Boy style LCD effect" },
8057
9004
  { id: "scanlines", name: "Scanlines", description: "Simple horizontal scanlines" }
8058
9005
  ];
8059
- var ShaderSelector = React5.memo(function ShaderSelector2({
9006
+ var ShaderSelector = React2.memo(function ShaderSelector2({
8060
9007
  currentShader,
8061
9008
  onShaderChange,
8062
9009
  disabled = false,
8063
9010
  systemColor = "#00FF41"
8064
9011
  }) {
8065
- const [isOpen, setIsOpen] = React5.useState(false);
8066
- const dropdownRef = React5.useRef(null);
8067
- React5.useEffect(() => {
9012
+ const [isOpen, setIsOpen] = React2.useState(false);
9013
+ const dropdownRef = React2.useRef(null);
9014
+ React2.useEffect(() => {
8068
9015
  const handleClickOutside = (e) => {
8069
9016
  if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
8070
9017
  setIsOpen(false);
@@ -8131,18 +9078,18 @@ var ShaderSelector = React5.memo(function ShaderSelector2({
8131
9078
  ] });
8132
9079
  });
8133
9080
  var ShaderSelector_default = ShaderSelector;
8134
- var SHORTCUTS2 = [
9081
+ var SHORTCUTS = [
8135
9082
  { key: "F1", description: "Help" },
8136
9083
  { key: "F3", description: "FPS Overlay" },
8137
9084
  { key: "F4", description: "Input Display" },
8138
9085
  { key: "F5", description: "Record" },
8139
9086
  { key: "F9", description: "Mute" }
8140
9087
  ];
8141
- var ShortcutsReference = React5.memo(function ShortcutsReference2({
9088
+ var ShortcutsReference = React2.memo(function ShortcutsReference2({
8142
9089
  systemColor = "#00FF41",
8143
9090
  isExpanded: initialExpanded = false
8144
9091
  }) {
8145
- const [isExpanded, setIsExpanded] = React5.useState(initialExpanded);
9092
+ const [isExpanded, setIsExpanded] = React2.useState(initialExpanded);
8146
9093
  return /* @__PURE__ */ jsxRuntime.jsxs(
8147
9094
  "div",
8148
9095
  {
@@ -8166,7 +9113,7 @@ var ShortcutsReference = React5.memo(function ShortcutsReference2({
8166
9113
  ]
8167
9114
  }
8168
9115
  ),
8169
- isExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 pb-3 pt-1 space-y-1.5 border-t border-white/10", children: SHORTCUTS2.map(({ key, description }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9116
+ isExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 pb-3 pt-1 space-y-1.5 border-t border-white/10", children: SHORTCUTS.map(({ key, description }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
8170
9117
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white/60", children: description }),
8171
9118
  /* @__PURE__ */ jsxRuntime.jsx(
8172
9119
  "kbd",
@@ -8199,6 +9146,7 @@ exports.DEFAULT_KEYBOARD = DEFAULT_KEYBOARD;
8199
9146
  exports.DPAD_BUTTONS = DPAD_BUTTONS;
8200
9147
  exports.FACE_BUTTONS = FACE_BUTTONS;
8201
9148
  exports.GamePlayer = GamePlayer_default;
9149
+ exports.KoinI18nProvider = KoinI18nProvider;
8202
9150
  exports.PERFORMANCE_TIER_1_SYSTEMS = PERFORMANCE_TIER_1_SYSTEMS;
8203
9151
  exports.PERFORMANCE_TIER_2_SYSTEMS = PERFORMANCE_TIER_2_SYSTEMS;
8204
9152
  exports.RASidebar = RASidebar;
@@ -8220,10 +9168,15 @@ exports.clearAllControls = clearAllControls;
8220
9168
  exports.consoleHasButton = consoleHasButton;
8221
9169
  exports.detectControllerBrand = detectControllerBrand;
8222
9170
  exports.detectSystem = detectSystem;
9171
+ exports.en = en;
9172
+ exports.es = es;
9173
+ exports.fetchAndCacheRom = fetchAndCacheRom;
8223
9174
  exports.formatGamepadButton = formatGamepadButton;
8224
9175
  exports.formatKeyCode = formatKeyCode;
9176
+ exports.fr = fr;
8225
9177
  exports.gamepadToRetroArchConfig = gamepadToRetroArchConfig;
8226
9178
  exports.getAchievementBadgeUrl = getAchievementBadgeUrl;
9179
+ exports.getCachedRom = getCachedRom;
8227
9180
  exports.getConsoleButtons = getConsoleButtons;
8228
9181
  exports.getConsoleCapabilities = getConsoleCapabilities;
8229
9182
  exports.getConsoleKeyboardDefaults = getConsoleKeyboardDefaults;
@@ -8249,6 +9202,7 @@ exports.saveKeyboardMapping = saveKeyboardMapping;
8249
9202
  exports.systemsMatch = systemsMatch;
8250
9203
  exports.useGameRecording = useGameRecording;
8251
9204
  exports.useGamepad = useGamepad;
9205
+ exports.useKoinTranslation = useKoinTranslation;
8252
9206
  exports.useNostalgist = useNostalgist;
8253
9207
  exports.useToast = useToast;
8254
9208
  //# sourceMappingURL=index.js.map