@enigma-lake/mines-play-controller-sdk 3.2.0 → 4.0.0

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.
Files changed (30) hide show
  1. package/dist/components/base/AutoPlayController/AutoPlayController.d.ts +1 -1
  2. package/dist/components/base/Button/Button.d.ts +1 -1
  3. package/dist/components/base/ChevronIcon/ChevronIcon.d.ts +1 -1
  4. package/dist/components/base/DifficultySelector/DifficultySelector.d.ts +1 -1
  5. package/dist/components/base/DifficultySelector/Selector.d.ts +1 -1
  6. package/dist/components/base/FreeRoundsIntroModal/FreeRoundsIntroModal.d.ts +2 -1
  7. package/dist/components/base/FreeRoundsSummaryModal/FreeRoundsSummaryModal.d.ts +1 -1
  8. package/dist/components/base/GroupRow/GroupRow.d.ts +1 -1
  9. package/dist/components/base/Input/Input.d.ts +1 -1
  10. package/dist/components/base/InputWithIcon/InputWithIcon.d.ts +1 -1
  11. package/dist/components/base/InputWithSwitch/InputWithSwitch.d.ts +1 -1
  12. package/dist/components/base/ManualPlayController/ManualPlayController.d.ts +1 -1
  13. package/dist/components/base/PlayController/PlayController.d.ts +1 -1
  14. package/dist/components/base/PlayValueInput/PlayValueInput.d.ts +1 -1
  15. package/dist/components/base/PlayValueList/PlayValueList.d.ts +1 -1
  16. package/dist/components/base/SelectMenu/GoldIcon.d.ts +1 -1
  17. package/dist/components/base/SelectMenu/SelectMenu.d.ts +1 -1
  18. package/dist/components/base/SelectMenu/SweepsIcon.d.ts +1 -1
  19. package/dist/components/base/Switch/Switch.d.ts +2 -1
  20. package/dist/i18n/I18nContext.d.ts +13 -0
  21. package/dist/i18n/index.d.ts +5 -0
  22. package/dist/i18n/locales/en.d.ts +2 -0
  23. package/dist/i18n/locales/zh-CN.d.ts +2 -0
  24. package/dist/i18n/translator.d.ts +6 -0
  25. package/dist/i18n/types.d.ts +30 -0
  26. package/dist/index.d.ts +67 -13
  27. package/dist/index.mjs +234 -64
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/types/playController.d.ts +7 -0
  30. package/package.json +9 -2
package/dist/index.mjs CHANGED
@@ -346,8 +346,135 @@ const SelectMenu = ({ disabled = false }) => {
346
346
  }) })] })) }));
347
347
  };
348
348
 
349
+ // Supported play-controller languages. Codes are BCP-47 style tags and line up
350
+ // 1:1 with the `code` column seeded in el-auth's `languages` catalog
351
+ // (en, zh-CN). Adding a language = add a code here + a locale file.
352
+ const SUPPORTED_LANGUAGE_CODES = ["en", "zh-CN"];
353
+
354
+ // English — the fallback language. Any key missing from another locale falls
355
+ // back to the value here.
356
+ const en = {
357
+ playNow: "PLAY NOW",
358
+ selectPlayAmount: "SELECT PLAY AMOUNT",
359
+ cashout: "CASHOUT",
360
+ collect: "COLLECT",
361
+ doubleTitle: "DOUBLE",
362
+ doubleSubtitle: "OR NOTHING",
363
+ playFreeRound: "PLAY FREE ROUND",
364
+ startAutoplay: "START AUTOPLAY",
365
+ stopAutoplay: "STOP AUTOPLAY",
366
+ startFreeAutoplay: "START FREE AUTOPLAY",
367
+ numberOfPlays: "Number of Plays",
368
+ selectTileToast: "Please select at least one tile to start autoplay.",
369
+ minesLabel: "Mines",
370
+ selectPlayAmountAria: "Select play amount",
371
+ freeRoundsModalTitle: "Free Game Rounds",
372
+ freeRoundsHave: "You have",
373
+ freeRoundsUnitOne: "FREE GAME ROUND",
374
+ freeRoundsUnitOther: "FREE GAME ROUNDS",
375
+ freeRoundsPerRound: "{amount} per round",
376
+ freeRoundsExpiresIn: "Expires in {days} {unit}",
377
+ dayUnitOne: "day",
378
+ dayUnitOther: "days",
379
+ freeRoundsModalPlayNow: "Play now",
380
+ freeRoundsModalPlayLater: "Maybe later",
381
+ };
382
+
383
+ // Simplified Chinese.
384
+ const zhCN = {
385
+ playNow: "立即游戏",
386
+ selectPlayAmount: "选择下注金额",
387
+ cashout: "提现",
388
+ collect: "收取",
389
+ doubleTitle: "双倍",
390
+ doubleSubtitle: "或全无",
391
+ playFreeRound: "免费游戏",
392
+ startAutoplay: "开始自动游戏",
393
+ stopAutoplay: "停止自动游戏",
394
+ startFreeAutoplay: "开始免费自动游戏",
395
+ numberOfPlays: "游戏次数",
396
+ selectTileToast: "请至少选择一个方块以开始自动游戏。",
397
+ minesLabel: "地雷",
398
+ selectPlayAmountAria: "选择下注金额",
399
+ freeRoundsModalTitle: "免费游戏回合",
400
+ freeRoundsHave: "您有",
401
+ freeRoundsUnitOne: "个免费游戏回合",
402
+ freeRoundsUnitOther: "个免费游戏回合",
403
+ freeRoundsPerRound: "每回合 {amount}",
404
+ freeRoundsExpiresIn: "{days} {unit}后到期",
405
+ dayUnitOne: "天",
406
+ dayUnitOther: "天",
407
+ freeRoundsModalPlayNow: "立即游戏",
408
+ freeRoundsModalPlayLater: "稍后再玩",
409
+ };
410
+
411
+ const DEFAULT_LANGUAGE = "en";
412
+ const SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGE_CODES;
413
+ // Central registry of locale bundles. Adding a language is: add its code to
414
+ // SUPPORTED_LANGUAGE_CODES, create a locale file, and register it here.
415
+ const dictionaries = {
416
+ en,
417
+ "zh-CN": zhCN,
418
+ };
419
+ // Normalize an arbitrary language input (e.g. "fr-FR", "ZH", "en-US") to one of
420
+ // the supported codes, falling back to English. Matches exact codes first, then
421
+ // by primary subtag ("fr-FR" -> "fr", "zh" -> "zh-CN").
422
+ const resolveLanguage = (input) => {
423
+ if (!input) {
424
+ return DEFAULT_LANGUAGE;
425
+ }
426
+ const normalized = input.trim().toLowerCase();
427
+ if (!normalized) {
428
+ return DEFAULT_LANGUAGE;
429
+ }
430
+ const exact = SUPPORTED_LANGUAGES.find((code) => code.toLowerCase() === normalized);
431
+ if (exact) {
432
+ return exact;
433
+ }
434
+ const primary = normalized.split("-")[0];
435
+ const byPrimary = SUPPORTED_LANGUAGES.find((code) => code.toLowerCase().split("-")[0] === primary);
436
+ return byPrimary ?? DEFAULT_LANGUAGE;
437
+ };
438
+ const interpolate = (template, params) => {
439
+ if (!params) {
440
+ return template;
441
+ }
442
+ return template.replace(/\{(\w+)\}/g, (match, key) => key in params ? String(params[key]) : match);
443
+ };
444
+ // Build a translator bound to a language. Per-key fallback to English ensures a
445
+ // partially-translated locale never renders a blank string.
446
+ const createTranslator = (language) => {
447
+ const dictionary = dictionaries[language] ?? dictionaries[DEFAULT_LANGUAGE];
448
+ const fallback = dictionaries[DEFAULT_LANGUAGE];
449
+ return (key, params) => {
450
+ const template = dictionary[key] ?? fallback[key] ?? key;
451
+ return interpolate(template, params);
452
+ };
453
+ };
454
+
455
+ const I18nContext = createContext(undefined);
456
+ // Provider placed at the root of the play-controller tree. `language` is the
457
+ // raw code coming from the host (UserInformation.language); it is normalized to
458
+ // a supported code here.
459
+ const I18nProvider = ({ language, children, }) => {
460
+ const value = useMemo(() => {
461
+ const resolved = resolveLanguage(language);
462
+ return { language: resolved, t: createTranslator(resolved) };
463
+ }, [language]);
464
+ return jsx(I18nContext.Provider, { value: value, children: children });
465
+ };
466
+ // Fallback value used when a component renders outside an I18nProvider (e.g. the
467
+ // standalone FreeRoundsIntroModal export). Defaults to English so nothing ever
468
+ // crashes or renders an empty label.
469
+ const DEFAULT_VALUE = {
470
+ language: DEFAULT_LANGUAGE,
471
+ t: createTranslator(DEFAULT_LANGUAGE),
472
+ };
473
+ const useTranslation = () => useContext(I18nContext) ?? DEFAULT_VALUE;
474
+
349
475
  const usePlayController = () => {
350
476
  const { config, autoPlay: { setNumberOfPlays, numberOfPlays, setPlayedRounds, playedRounds, selection, setState, state, }, playValues: { displayPlayAmountView, setDisplayPlayAmountView, togglePlayAmountView, }, } = useAutoManualPlayState();
477
+ const { t } = useTranslation();
351
478
  const { current } = config.currencyOptions;
352
479
  const { isPlaying, canCashout, disabledController, showAutoPlayToast, autoPlayDelay = 1500, playHook, } = config.playOptions;
353
480
  const { playAmount: rawPlayAmount, playLimits, setPlayAmount: rawSetPlayAmount, } = playHook?.() ?? {};
@@ -380,6 +507,17 @@ const usePlayController = () => {
380
507
  engaged: Boolean(freeRoundsOptions?.engaged),
381
508
  remaining: freeRoundsOptions?.roundsRemaining ?? 0,
382
509
  };
510
+ // Per-session accounting for engaged autoplay. roundsRemaining is game-driven
511
+ // and only decrements via an async refetch AFTER each settlement, so the live
512
+ // ref can still read stale-high when the loop schedules the next round. We
513
+ // therefore also count submissions locally: a session that started engaged
514
+ // may never submit more rounds than the grants available when it started,
515
+ // regardless of refetch timing.
516
+ const autoplaySessionRef = useRef({
517
+ startedEngaged: false,
518
+ grantsAtStart: 0,
519
+ roundsSubmitted: 0,
520
+ });
383
521
  const stopAutoplay = () => {
384
522
  isAutoplayActiveRef.current = false;
385
523
  if (playIntervalRef.current) {
@@ -397,11 +535,23 @@ const usePlayController = () => {
397
535
  if (!isAutoplayActiveRef.current) {
398
536
  return;
399
537
  }
400
- // Hard-stop: free-rounds autoplay ends the moment the grants are exhausted.
401
- if (freeRoundsRef.current.engaged && freeRoundsRef.current.remaining <= 0) {
402
- setNumberOfPlays(AUTOPLAY_DEFAULT_PLAY_ROUNDS_COUNT);
403
- stopAutoplay();
404
- return;
538
+ // Hard-stop: an autoplay session that started on free rounds ends the
539
+ // moment the grants are exhausted or engagement ends mid-session — it must
540
+ // never submit a paid round. Checked BEFORE each submission so the round
541
+ // that consumes the last grant completes normally, then the loop halts.
542
+ // Reads live values via refs (closures here are stale across rounds) plus
543
+ // the local per-session submission count (the game's roundsRemaining
544
+ // refetch can lag behind the next scheduled round).
545
+ const session = autoplaySessionRef.current;
546
+ if (session.startedEngaged) {
547
+ const live = freeRoundsRef.current;
548
+ const grantsExhausted = session.roundsSubmitted >= session.grantsAtStart ||
549
+ live.remaining <= 0;
550
+ if (grantsExhausted || !live.engaged) {
551
+ setNumberOfPlays(AUTOPLAY_DEFAULT_PLAY_ROUNDS_COUNT);
552
+ stopAutoplay();
553
+ return;
554
+ }
405
555
  }
406
556
  if (remainingPlays < 1 || numberOfPlays === 0) {
407
557
  setNumberOfPlays(AUTOPLAY_DEFAULT_PLAY_ROUNDS_COUNT);
@@ -410,6 +560,7 @@ const usePlayController = () => {
410
560
  }
411
561
  setPlayedRounds(currentPlayedRounds + 1);
412
562
  setNumberOfPlays((prev) => Math.max(prev - 1, 0));
563
+ session.roundsSubmitted += 1;
413
564
  config.onAutoPlay(selection, () => {
414
565
  if (!isAutoplayActiveRef.current) {
415
566
  return;
@@ -424,7 +575,7 @@ const usePlayController = () => {
424
575
  if (selection.length === 0) {
425
576
  showAutoPlayToast?.({
426
577
  type: "info",
427
- message: "Please select at least one tile to start autoplay.",
578
+ message: t("selectTileToast"),
428
579
  });
429
580
  return;
430
581
  }
@@ -438,6 +589,15 @@ const usePlayController = () => {
438
589
  if (isFreeRoundsEngaged && effectiveNumberOfPlays !== numberOfPlays) {
439
590
  setNumberOfPlays(effectiveNumberOfPlays);
440
591
  }
592
+ // Snapshot the free-rounds state for this session. Uses the RAW engaged
593
+ // flag (not isFreeRoundsEngaged, which also requires remaining > 0) so a
594
+ // session started while engaged with 0 grants fails closed on the first
595
+ // loop iteration instead of silently becoming a paid session.
596
+ autoplaySessionRef.current = {
597
+ startedEngaged: Boolean(freeRoundsOptions?.engaged),
598
+ grantsAtStart: freeRoundsOptions?.roundsRemaining ?? 0,
599
+ roundsSubmitted: 0,
600
+ };
441
601
  isAutoplayActiveRef.current = true;
442
602
  setState(AUTO_PLAY_STATE.PLAYING);
443
603
  loopRounds(playedRounds, effectiveNumberOfPlays);
@@ -571,12 +731,6 @@ const PlayAmountControl = ({ isDisabled }) => {
571
731
  state === AUTO_PLAY_STATE.PLAYING, onClick: handleTogglePlayAmount, isClassicGame: isClassicGame, children: jsx(SelectMenu, { disabled: isDisabled() }) }), jsx(Button, { className: styles_group.groupItem, onClick: () => adjustPlayAmount({ direction: -1 }), theme: "ghost", disabled: isDisabled(), children: jsx("span", { className: styles_group.x2, children: "-" }) }), jsx(Button, { className: styles_group.groupItem, onClick: () => adjustPlayAmount({ direction: 1 }), theme: "ghost", disabled: isDisabled(), children: jsx("span", { className: cx$1(styles_group.x2, styles_group.last), children: "+" }) })] }));
572
732
  };
573
733
 
574
- var AUTOPLAY_LABEL;
575
- (function (AUTOPLAY_LABEL) {
576
- AUTOPLAY_LABEL["START"] = "START AUTOPLAY";
577
- AUTOPLAY_LABEL["STOP"] = "STOP AUTOPLAY";
578
- })(AUTOPLAY_LABEL || (AUTOPLAY_LABEL = {}));
579
-
580
734
  function clamp01(value) {
581
735
  if (value < 0) {
582
736
  return 0;
@@ -721,6 +875,7 @@ function usePlayControllerButtons(params) {
721
875
 
722
876
  const AutoPlayController = () => {
723
877
  const { config } = useAutoManualPlayState();
878
+ const { t } = useTranslation();
724
879
  const { isValidPlayAmount, freeRounds, playValues: { displayPlayAmountView, togglePlayAmountView }, autoPlay: { isDisabled, state, onPlay, onStopPlay }, } = usePlayController();
725
880
  const { current } = config.currencyOptions;
726
881
  const roleButton = GAME_MODE.AUTOPLAY;
@@ -770,10 +925,10 @@ const AutoPlayController = () => {
770
925
  }, [state]);
771
926
  const buttonFallbackLabel = useMemo(() => {
772
927
  if (state === AUTO_PLAY_STATE.PLAYING) {
773
- return AUTOPLAY_LABEL.STOP;
928
+ return t("stopAutoplay");
774
929
  }
775
- return AUTOPLAY_LABEL.START;
776
- }, [state]);
930
+ return t("startAutoplay");
931
+ }, [state, t]);
777
932
  const handleKeyPress = useCallback((event) => {
778
933
  if (event.code !== "Space") {
779
934
  return;
@@ -856,7 +1011,7 @@ const AutoPlayController = () => {
856
1011
  const renderButton = useMemo(() => {
857
1012
  if (displayPlayAmountView) {
858
1013
  const id = "selectPlayAmount";
859
- const label = buttons.resolveLabel(id, "SELECT PLAY AMOUNT");
1014
+ const label = buttons.resolveLabel(id, t("selectPlayAmount"));
860
1015
  const style = buttons.resolveStyle(id, {
861
1016
  pressed: buttons.pressedId === id,
862
1017
  disabled: false,
@@ -874,7 +1029,7 @@ const AutoPlayController = () => {
874
1029
  // STOP button keeps its cashout styling and gains a remaining-rounds
875
1030
  // count: "STOP AUTOPLAY (N)".
876
1031
  if (freeRounds.isEngaged && isStart) {
877
- return (jsx(Button, { disabled: isButtonDisabled, className: styles_button.buttonFree, style: style, ...buttons.getPressHandlers(buttonId), onClick: buttonAction, roleType: roleButton, children: "START FREE AUTOPLAY" }));
1032
+ return (jsx(Button, { disabled: isButtonDisabled, className: styles_button.buttonFree, style: style, ...buttons.getPressHandlers(buttonId), onClick: buttonAction, roleType: roleButton, children: t("startFreeAutoplay") }));
878
1033
  }
879
1034
  let label = buttons.resolveLabel(buttonId, buttonFallbackLabel);
880
1035
  // Free-rounds engaged: show the remaining grant count on the STOP button
@@ -900,6 +1055,7 @@ const AutoPlayController = () => {
900
1055
  handleTogglePlayAmount,
901
1056
  isButtonDisabled,
902
1057
  roleButton,
1058
+ t,
903
1059
  ]);
904
1060
  return (jsxs(Fragment$1, { children: [jsx(PlayAmountControl, { isDisabled: () => isDisabled() || freeRounds.isEngaged }), renderButton] }));
905
1061
  };
@@ -929,6 +1085,7 @@ const useIsRunningExternal = () => {
929
1085
  const ManualPlayController = () => {
930
1086
  const { config, doubleOrNothing } = useAutoManualPlayState();
931
1087
  const { isRunningExternal } = useIsRunningExternal();
1088
+ const { t } = useTranslation();
932
1089
  const { isValidPlayAmount, freeRounds, playValues: { displayPlayAmountView, togglePlayAmountView }, manualPlay: { isDisabled, onPlay, canCashout }, } = usePlayController();
933
1090
  const { current } = config.currencyOptions;
934
1091
  const roleButton = GAME_MODE.MANUAL;
@@ -1079,7 +1236,7 @@ const ManualPlayController = () => {
1079
1236
  const renderButton = useMemo(() => {
1080
1237
  if (displayPlayAmountView) {
1081
1238
  const id = "selectPlayAmount";
1082
- const label = buttons.resolveLabel(id, "SELECT PLAY AMOUNT");
1239
+ const label = buttons.resolveLabel(id, t("selectPlayAmount"));
1083
1240
  const style = buttons.resolveStyle(id, {
1084
1241
  pressed: buttons.pressedId === id,
1085
1242
  disabled: false,
@@ -1095,7 +1252,7 @@ const ManualPlayController = () => {
1095
1252
  const isDoubleOrNothingDisabled = isCashoutDisabled ||
1096
1253
  !!config.doubleOrNothing?.disabled ||
1097
1254
  doubleOrNothing.state === DOUBLE_OR_NOTHING_STATE.PROGRESS;
1098
- const label = buttons.resolveLabel(id, isRunningExternal ? "COLLECT" : "CASHOUT");
1255
+ const label = buttons.resolveLabel(id, isRunningExternal ? t("collect") : t("cashout"));
1099
1256
  const style = buttons.resolveStyle(id, {
1100
1257
  pressed: buttons.pressedId === id,
1101
1258
  disabled: isCashoutDisabled,
@@ -1104,7 +1261,7 @@ const ManualPlayController = () => {
1104
1261
  [styles_button.buttonRow__cashout]: showDoubleOrNothing,
1105
1262
  }), style: style, ...buttons.getPressHandlers(id), onClick: handleCashoutClick, roleType: roleButton, children: [label, " ", format(current.possibleWin ?? 0, current.decimals), " ", current.abbr] }), showDoubleOrNothing ? (jsx(Button, { disabled: isDoubleOrNothingDisabled, className: cx$1(styles_button.buttonDON, styles_button.buttonRow__don, {
1106
1263
  [styles_button.buttonDON__disabled]: isDoubleOrNothingDisabled,
1107
- }), onClick: handleDoubleOrNothingClick, roleType: roleButton, children: jsxs("div", { className: styles_button.buttonDON__content, children: [jsx("p", { className: styles_button.buttonDON__title, children: "DOUBLE" }), jsx("p", { className: styles_button.buttonDON__subtitle, children: "OR NOTHING" })] }) })) : null] }));
1264
+ }), onClick: handleDoubleOrNothingClick, roleType: roleButton, children: jsxs("div", { className: styles_button.buttonDON__content, children: [jsx("p", { className: styles_button.buttonDON__title, children: t("doubleTitle") }), jsx("p", { className: styles_button.buttonDON__subtitle, children: t("doubleSubtitle") })] }) })) : null] }));
1108
1265
  }
1109
1266
  {
1110
1267
  // Game-driven autoplay loop running: the stop-autoplay control owns the
@@ -1121,9 +1278,9 @@ const ManualPlayController = () => {
1121
1278
  // free round. No count on the button — the remaining count lives only
1122
1279
  // in the footer status line.
1123
1280
  if (freeRounds.isEngaged) {
1124
- return (jsx(Button, { disabled: isButtonDisabled, className: styles_button.buttonFree, style: style, ...buttons.getPressHandlers(id), onClick: onPlay, roleType: roleButton, children: "PLAY FREE ROUND" }));
1281
+ return (jsx(Button, { disabled: isButtonDisabled, className: styles_button.buttonFree, style: style, ...buttons.getPressHandlers(id), onClick: onPlay, roleType: roleButton, children: t("playFreeRound") }));
1125
1282
  }
1126
- const label = buttons.resolveLabel(id, "PLAY NOW", {
1283
+ const label = buttons.resolveLabel(id, t("playNow"), {
1127
1284
  disabled: isButtonDisabled,
1128
1285
  isPlaying: config.playOptions.isPlaying,
1129
1286
  });
@@ -1152,6 +1309,7 @@ const ManualPlayController = () => {
1152
1309
  isRunningExternal,
1153
1310
  onPlay,
1154
1311
  roleButton,
1312
+ t,
1155
1313
  ]);
1156
1314
  return (jsxs(Fragment$1, { children: [jsx(PlayAmountControl, { isDisabled: () => isDisabled() || displayPlayAmountView || freeRounds.isEngaged }), renderButton] }));
1157
1315
  };
@@ -1162,8 +1320,9 @@ var styles$3 = {"backdrop":"FreeRoundsIntroModal-module__backdrop___qIgNx","cont
1162
1320
  // callbacks only; the SDK does no fetching. `onPlayNow` should engage
1163
1321
  // free-rounds mode + close, `onPlayLater` should close and render the
1164
1322
  // controller exactly as if the user had no free rounds for this session.
1165
- const FreeRoundsIntroModal = ({ open, roundsRemaining, stakeCents, coinType, currency, decimals = 2, expiresAt, onPlayNow, onPlayLater, }) => {
1323
+ const FreeRoundsIntroModal = ({ open, roundsRemaining, stakeCents, coinType, currency, decimals = 2, expiresAt, language, onPlayNow, onPlayLater, }) => {
1166
1324
  const handleClose = () => onPlayLater?.();
1325
+ const t = useMemo(() => createTranslator(resolveLanguage(language)), [language]);
1167
1326
  const coinLabel = resolveFreeRoundsCoinLabel({ coinType, currency });
1168
1327
  const valueDecimals = resolveFreeRoundsDecimals({
1169
1328
  coinType,
@@ -1171,8 +1330,13 @@ const FreeRoundsIntroModal = ({ open, roundsRemaining, stakeCents, coinType, cur
1171
1330
  fallback: decimals,
1172
1331
  });
1173
1332
  const valueDisplay = format(stakeCents / 100, valueDecimals);
1333
+ const valueAmount = `${valueDisplay}${coinLabel ? ` ${coinLabel}` : ""}`;
1174
1334
  const expiresInDays = daysUntil(expiresAt);
1175
- return (jsx(Dialog, { open: open, onClose: handleClose, className: styles$3.backdrop, children: jsx("div", { className: styles$3.content, children: jsxs(DialogPanel, { className: styles$3.panel, children: [jsx(DialogTitle, { className: styles$3.header, children: "Free Game Rounds" }), jsxs("div", { className: styles$3.summary, children: ["You have ", jsx("span", { className: styles$3.accent, children: roundsRemaining }), " ", roundsRemaining === 1 ? "FREE GAME ROUND" : "FREE GAME ROUNDS"] }), jsxs("div", { className: styles$3.perRound, children: [valueDisplay, coinLabel ? ` ${coinLabel}` : "", " per round"] }), expiresInDays !== null ? (jsxs("div", { className: styles$3.expiry, children: ["Expires in ", expiresInDays, " ", expiresInDays === 1 ? "day" : "days"] })) : null, jsxs("div", { className: styles$3.actions, children: [jsx("button", { type: "button", className: `${styles$3.button} ${styles$3.playNow}`, onClick: onPlayNow, children: "Play now" }), jsx("button", { type: "button", className: `${styles$3.button} ${styles$3.playLater}`, onClick: onPlayLater, children: "Maybe later" })] })] }) }) }));
1335
+ const roundsUnit = roundsRemaining === 1 ? t("freeRoundsUnitOne") : t("freeRoundsUnitOther");
1336
+ return (jsx(Dialog, { open: open, onClose: handleClose, className: styles$3.backdrop, children: jsx("div", { className: styles$3.content, children: jsxs(DialogPanel, { className: styles$3.panel, children: [jsx(DialogTitle, { className: styles$3.header, children: t("freeRoundsModalTitle") }), jsxs("div", { className: styles$3.summary, children: [t("freeRoundsHave"), " ", jsx("span", { className: styles$3.accent, children: roundsRemaining }), " ", roundsUnit] }), jsx("div", { className: styles$3.perRound, children: t("freeRoundsPerRound", { amount: valueAmount }) }), expiresInDays !== null ? (jsx("div", { className: styles$3.expiry, children: t("freeRoundsExpiresIn", {
1337
+ days: expiresInDays,
1338
+ unit: expiresInDays === 1 ? t("dayUnitOne") : t("dayUnitOther"),
1339
+ }) })) : null, jsxs("div", { className: styles$3.actions, children: [jsx("button", { type: "button", className: `${styles$3.button} ${styles$3.playNow}`, onClick: onPlayNow, children: t("freeRoundsModalPlayNow") }), jsx("button", { type: "button", className: `${styles$3.button} ${styles$3.playLater}`, onClick: onPlayLater, children: t("freeRoundsModalPlayLater") })] })] }) }) }));
1176
1340
  };
1177
1341
 
1178
1342
  var styles_ui = {"base":"UI-module__base___wThyQ","container":"UI-module__container___gMSiK","betForm":"UI-module__betForm___hQkYd","isClassicGame":"UI-module__isClassicGame___zV1we","containerSpread":"UI-module__containerSpread___Axb7e","baseForceLeft":"UI-module__baseForceLeft___xfc-h","disabled":"UI-module__disabled___dnZJX","auto":"UI-module__auto___5peb8","freeRoundsFooter":"UI-module__freeRoundsFooter___AOprS","freeRoundsFooter__count":"UI-module__freeRoundsFooter__count___eHp9g","controllerWidgets":"UI-module__controllerWidgets___x2lBy","sideWidget":"UI-module__sideWidget___WMGRy","centerWidget":"UI-module__centerWidget___sLLUi","left":"UI-module__left___1wtUp","center":"UI-module__center___E0ILH","right":"UI-module__right___yLHvk","controls":"UI-module__controls___Z6L-V"};
@@ -1282,6 +1446,7 @@ var styles$2 = {"base":"PlayValueList-module__base___5duc6","playValuesWrapper":
1282
1446
 
1283
1447
  const PlayValueList = () => {
1284
1448
  const { config } = useAutoManualPlayState();
1449
+ const { t } = useTranslation();
1285
1450
  const isClassicGame = config?.isClassicGame ?? false;
1286
1451
  const { onChangeAmount, playAmount, totalBalance, playValues: { displayPlayAmountView, togglePlayAmountView }, } = usePlayController();
1287
1452
  const { playLimits } = config.playOptions.playHook();
@@ -1316,7 +1481,7 @@ const PlayValueList = () => {
1316
1481
  applyValue(value);
1317
1482
  }
1318
1483
  };
1319
- return (jsx("div", { className: cx$1(styles$2.base, { [styles$2.isClassicGame]: isClassicGame }), children: jsx("div", { className: cx$1(styles$2.playValuesWrapper), onClick: handleClick, role: "listbox", "aria-label": "Select play amount", "data-testid": "play-value-list", children: playLimits?.[config.currencyOptions.current.code].defaultBetOptions.map((playValue) => (jsx("div", { className: cx$1(styles$2.playValue, {
1484
+ return (jsx("div", { className: cx$1(styles$2.base, { [styles$2.isClassicGame]: isClassicGame }), children: jsx("div", { className: cx$1(styles$2.playValuesWrapper), onClick: handleClick, role: "listbox", "aria-label": t("selectPlayAmountAria"), "data-testid": "play-value-list", children: playLimits?.[config.currencyOptions.current.code].defaultBetOptions.map((playValue) => (jsx("div", { className: cx$1(styles$2.playValue, {
1320
1485
  [styles$2.selectedPlayValue]: playValue === playAmount,
1321
1486
  [styles$2.disabledPlayValue]: playValue > totalBalance,
1322
1487
  }), "data-value": playValue, role: "option", tabIndex: 0, "aria-selected": false, children: format(playValue, config.currencyOptions.current.decimals) }, `${config.currencyOptions.current.code}-${playValue}`))) }) }));
@@ -1379,6 +1544,7 @@ const generateMines = (numberOfMines) => {
1379
1544
  };
1380
1545
 
1381
1546
  const DifficultySelector = ({ playOptions, }) => {
1547
+ const { t } = useTranslation();
1382
1548
  const classicGame = playOptions?.classicGame;
1383
1549
  const disabledController = playOptions.disabledController;
1384
1550
  const disabledMenu = classicGame?.disabledMenu ?? false;
@@ -1393,7 +1559,7 @@ const DifficultySelector = ({ playOptions, }) => {
1393
1559
  }
1394
1560
  onMinesChange(value);
1395
1561
  }
1396
- return (jsx("div", { className: style_difficulty.base, children: jsx(Selector, { currentValue: currentMines, label: SELECTORS.MINES, values: mines, onSelect: onMinesSelected, disabled: disabledMenu || disabledController }) }));
1562
+ return (jsx("div", { className: style_difficulty.base, children: jsx(Selector, { currentValue: currentMines, label: t("minesLabel"), values: mines, onSelect: onMinesSelected, disabled: disabledMenu || disabledController }) }));
1397
1563
  };
1398
1564
 
1399
1565
  const AutoManualPlayProvider = ({ children, config, }) => {
@@ -1420,6 +1586,10 @@ const AutoManualPlayProvider = ({ children, config, }) => {
1420
1586
  if (isFreeRoundsEngaged) {
1421
1587
  displayedNumberOfPlays = Math.min(displayedNumberOfPlays, freeRoundsRemaining);
1422
1588
  }
1589
+ // Local translator for strings rendered by the provider itself. The provider
1590
+ // sits above the <I18nProvider> it renders, so it cannot use useTranslation();
1591
+ // descendant components (controllers, modal) use the context hook instead.
1592
+ const t = useMemo(() => createTranslator(resolveLanguage(config.language)), [config.language]);
1423
1593
  const togglePlayAmountView = useCallback(() => {
1424
1594
  setDisplayPlayAmountView((v) => !v);
1425
1595
  }, []);
@@ -1539,43 +1709,43 @@ const AutoManualPlayProvider = ({ children, config, }) => {
1539
1709
  displayPlayAmountView,
1540
1710
  togglePlayAmountView,
1541
1711
  ]);
1542
- return (jsxs(AutoManualPlayStateContext.Provider, { value: contextValue, children: [typeof children === "function" ? children(contextValue) : children, config.playOptions.displayController && (jsx("div", { className: cx$1(styles_ui.base, styles_ui.betForm, {
1543
- [styles_ui.baseForceLeft]: config?.classicGame?.forceLeftStyle,
1544
- [styles_ui.isClassicGame]: config?.isClassicGame,
1545
- }), style: {
1546
- "--play-bottom": config.panel?.bottom ?? 0,
1547
- "--play-panel-bg": hexToRgb(config.panel.bgColorHex ?? "#01243A"),
1548
- "--play-panel-bg-opacity": config.panel.opacity ?? 0.5,
1549
- "--play-dropdown-bg": hexToRgb(config?.classicGame?.dropdown?.bgColorHex ?? "#01243A"),
1550
- }, children: jsxs("div", { className: cx$1(styles_ui.container, {
1551
- [styles_ui.containerSpread]: config?.classicGame?.forceLeftStyle,
1552
- }), children: [jsxs("div", { className: cx$1(styles_ui.controls), children: [jsx(PlayValueList, {}), jsxs("div", { className: cx$1(styles_ui.auto), children: [config.isClassicGame ? (jsx(DifficultySelector, { playOptions: {
1553
- ...config.playOptions,
1554
- } })) : null, jsx(InputWithSwitch, { value: displayedNumberOfPlays, type: "number", onChange: (e) => {
1555
- const next = Number(e.currentTarget.value);
1556
- setNumberOfPlays(isFreeRoundsEngaged
1557
- ? Math.min(next, freeRoundsRemaining)
1558
- : next);
1559
- }, placeholder: "Number of Plays", min: 0, max: isFreeRoundsEngaged ? freeRoundsRemaining : 99, disabled: config.playOptions.disabledController ||
1560
- mode === GAME_MODE.MANUAL, switcherConfig: {
1561
- onSwitch: toggleMode,
1562
- isPlaying: isAutoPlaying || config.playOptions.isPlaying,
1563
- enabled: mode !== GAME_MODE.MANUAL,
1564
- disabled: config.playOptions.disabledController ||
1565
- autoplayState === AUTO_PLAY_STATE.PLAYING,
1566
- }, isClassicGame: config.isClassicGame, children: jsx("span", { className: cx$1({
1567
- [styles_ui.disabled]: mode !== GAME_MODE.AUTOPLAY ||
1568
- numberOfPlays !== Infinity ||
1569
- autoplayState === AUTO_PLAY_STATE.PLAYING ||
1570
- // Free-rounds autoplay is exactly grant-count: no infinite.
1571
- isFreeRoundsEngaged,
1572
- }), children: "\u221E" }) })] }), mode === GAME_MODE.MANUAL ? (jsx(ManualPlayController, {})) : (jsx(AutoPlayController, {})), isFreeRoundsEngaged ? (jsxs("div", { className: styles_ui.freeRoundsFooter, children: [jsx("span", { className: styles_ui.freeRoundsFooter__count, children: freeRoundsRemaining }), " ", freeRoundsRemaining === 1
1573
- ? "FREE ROUND LEFT"
1574
- : "FREE ROUNDS LEFT"] })) : null] }), freeRoundsOptions ? (jsx(FreeRoundsIntroModal, { open: showFreeRoundsIntroModal, roundsRemaining: freeRoundsRemaining, totalRounds: freeRoundsOptions.totalRounds, stakeCents: freeRoundsOptions.stakeCents, coinType: freeRoundsOptions.coinType, currency: freeRoundsOptions.currency, decimals: config.currencyOptions.current.decimals, expiresAt: freeRoundsOptions.expiresAt, onPlayNow: freeRoundsOptions.onPlayNow, onPlayLater: freeRoundsOptions.onPlayLater })) : null, jsx(WidgetContainer, { state: autoplayState, displayPlayAmountView: displayPlayAmountView, widgets: {
1575
- left: config.leftWidgets,
1576
- center: config.centerWidgets,
1577
- right: config.rightWidgets,
1578
- } })] }) }))] }));
1712
+ return (jsx(I18nProvider, { language: config.language, children: jsxs(AutoManualPlayStateContext.Provider, { value: contextValue, children: [typeof children === "function" ? children(contextValue) : children, config.playOptions.displayController && (jsx("div", { className: cx$1(styles_ui.base, styles_ui.betForm, {
1713
+ [styles_ui.baseForceLeft]: config?.classicGame?.forceLeftStyle,
1714
+ [styles_ui.isClassicGame]: config?.isClassicGame,
1715
+ }), style: {
1716
+ "--play-bottom": config.panel?.bottom ?? 0,
1717
+ "--play-panel-bg": hexToRgb(config.panel.bgColorHex ?? "#01243A"),
1718
+ "--play-panel-bg-opacity": config.panel.opacity ?? 0.5,
1719
+ "--play-dropdown-bg": hexToRgb(config?.classicGame?.dropdown?.bgColorHex ?? "#01243A"),
1720
+ }, children: jsxs("div", { className: cx$1(styles_ui.container, {
1721
+ [styles_ui.containerSpread]: config?.classicGame?.forceLeftStyle,
1722
+ }), children: [jsxs("div", { className: cx$1(styles_ui.controls), children: [jsx(PlayValueList, {}), jsxs("div", { className: cx$1(styles_ui.auto), children: [config.isClassicGame ? (jsx(DifficultySelector, { playOptions: {
1723
+ ...config.playOptions,
1724
+ } })) : null, jsx(InputWithSwitch, { value: displayedNumberOfPlays, type: "number", onChange: (e) => {
1725
+ const next = Number(e.currentTarget.value);
1726
+ setNumberOfPlays(isFreeRoundsEngaged
1727
+ ? Math.min(next, freeRoundsRemaining)
1728
+ : next);
1729
+ }, placeholder: t("numberOfPlays"), min: 0, max: isFreeRoundsEngaged ? freeRoundsRemaining : 99, disabled: config.playOptions.disabledController ||
1730
+ mode === GAME_MODE.MANUAL, switcherConfig: {
1731
+ onSwitch: toggleMode,
1732
+ isPlaying: isAutoPlaying || config.playOptions.isPlaying,
1733
+ enabled: mode !== GAME_MODE.MANUAL,
1734
+ disabled: config.playOptions.disabledController ||
1735
+ autoplayState === AUTO_PLAY_STATE.PLAYING,
1736
+ }, isClassicGame: config.isClassicGame, children: jsx("span", { className: cx$1({
1737
+ [styles_ui.disabled]: mode !== GAME_MODE.AUTOPLAY ||
1738
+ numberOfPlays !== Infinity ||
1739
+ autoplayState === AUTO_PLAY_STATE.PLAYING ||
1740
+ // Free-rounds autoplay is exactly grant-count: no infinite.
1741
+ isFreeRoundsEngaged,
1742
+ }), children: "\u221E" }) })] }), mode === GAME_MODE.MANUAL ? (jsx(ManualPlayController, {})) : (jsx(AutoPlayController, {})), isFreeRoundsEngaged ? (jsxs("div", { className: styles_ui.freeRoundsFooter, children: [jsx("span", { className: styles_ui.freeRoundsFooter__count, children: freeRoundsRemaining }), " ", freeRoundsRemaining === 1
1743
+ ? "FREE ROUND LEFT"
1744
+ : "FREE ROUNDS LEFT"] })) : null] }), freeRoundsOptions ? (jsx(FreeRoundsIntroModal, { open: showFreeRoundsIntroModal, roundsRemaining: freeRoundsRemaining, totalRounds: freeRoundsOptions.totalRounds, stakeCents: freeRoundsOptions.stakeCents, coinType: freeRoundsOptions.coinType, currency: freeRoundsOptions.currency, decimals: config.currencyOptions.current.decimals, expiresAt: freeRoundsOptions.expiresAt, onPlayNow: freeRoundsOptions.onPlayNow, onPlayLater: freeRoundsOptions.onPlayLater })) : null, jsx(WidgetContainer, { state: autoplayState, displayPlayAmountView: displayPlayAmountView, widgets: {
1745
+ left: config.leftWidgets,
1746
+ center: config.centerWidgets,
1747
+ right: config.rightWidgets,
1748
+ } })] }) }))] }) }));
1579
1749
  };
1580
1750
 
1581
1751
  var styles = {"backdrop":"FreeRoundsSummaryModal-module__backdrop___tNi3I","panel":"FreeRoundsSummaryModal-module__panel___2MIEp","header":"FreeRoundsSummaryModal-module__header___-BkpP","summary":"FreeRoundsSummaryModal-module__summary___YExX9","accent":"FreeRoundsSummaryModal-module__accent___z9aBF","hint":"FreeRoundsSummaryModal-module__hint___CYWqA","close":"FreeRoundsSummaryModal-module__close___4-irR"};
@@ -1593,5 +1763,5 @@ const FreeRoundsSummaryModal = ({ totalWinAmount, coin, roundsPlayed, decimals,
1593
1763
  return (jsx(Dialog, { open: true, onClose: onClose, className: styles.backdrop, children: jsxs(DialogPanel, { className: styles.panel, children: [jsx(DialogTitle, { className: styles.header, children: "Free Game Rounds" }), jsxs("div", { className: styles.summary, children: ["You won", " ", jsxs("span", { className: styles.accent, children: [amountDisplay, " ", coin] }), " ", "in ", roundsPlayed, " ", roundsPlayed === 1 ? "free game round" : "free game rounds"] }), jsx("div", { className: styles.hint, children: "Winnings have been credited to your bonus balance." }), jsx("button", { type: "button", className: styles.close, onClick: onClose, children: "Close" })] }) }));
1594
1764
  };
1595
1765
 
1596
- export { AUTO_PLAY_STATE, AutoManualPlayProvider, DOUBLE_OR_NOTHING_STATE, FreeRoundsIntroModal, FreeRoundsSummaryModal, GAME_MODE, WIDGET, format };
1766
+ export { AUTO_PLAY_STATE, AutoManualPlayProvider, DEFAULT_LANGUAGE, DOUBLE_OR_NOTHING_STATE, FreeRoundsIntroModal, FreeRoundsSummaryModal, GAME_MODE, I18nProvider, SUPPORTED_LANGUAGES, SUPPORTED_LANGUAGE_CODES, WIDGET, createTranslator, format, resolveLanguage, useTranslation };
1597
1767
  //# sourceMappingURL=index.mjs.map