diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index 1a7ac500..fe55c561 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -14,7 +14,12 @@ "refuse": "Ablehnen", "apply": "Übernehmen", "copy": "Kopieren", - "copied": "Kopiert!" + "copied": "Kopiert!", + "confirm": "Bestätigen", + "choose-folder": "Ordner Wählen", + "unknown": "Ein unbekannter Fehler ist aufgetreten ¯\\_(ツ)_/¯", + "warning": "Achtung", + "continue": "Weiter" }, "nav-bar": { "add-version": "Version Hinzufügen", @@ -735,6 +740,9 @@ }, "launch-as-admin": "Als Administrator starten", "not-remind-me": "Nicht mehr erinnern" + }, + "bs-version-outdated": { + "body": "Die Version {outdatedVersion} ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht mehr wie erwartet. Bitte lade die neueste empfohlene Version ({recommendedVersion}) von Beat Saber herunter, um die neuesten Funktionen und Fehlerbehebungen zu nutzen." } }, "maps": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index c594eb82..d7cc55ad 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -14,7 +14,12 @@ "refuse": "Refuse", "apply": "Apply", "copy": "Copy", - "copied": "Copied!" + "copied": "Copied!", + "confirm": "Confirm", + "choose-folder": "Choose folder", + "unknown": "An unknown error occurred ¯\\_(ツ)_/¯", + "warning": "Warning", + "continue": "Continue" }, "nav-bar": { "add-version": "Add a version", @@ -735,6 +740,9 @@ }, "launch-as-admin": "Launch as Administrator", "not-remind-me": "Do not remind me" + }, + "bs-version-outdated": { + "body": "The version {outdatedVersion} is outdated, and some mods or features may no longer work as expected. Please download the latest recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes." } }, "maps": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index d0f6fee3..aac255da 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -14,7 +14,12 @@ "refuse": "Rechazar", "apply": "Aplicar", "copy": "Copiar", - "copied": "¡Copiado!" + "copied": "¡Copiado!", + "confirm": "Confirmar", + "choose-folder": "Elige la carpeta", + "unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯", + "warning": "Advertencia", + "continue": "Continuar" }, "nav-bar": { "add-version": "Agregar una versión", @@ -735,6 +740,9 @@ }, "launch-as-admin": "Iniciar como Administrador", "not-remind-me": "No volver a recordármelo" + }, + "bs-version-outdated": { + "body": "La versión {outdatedVersion} está obsoleta, y algunos mods o funciones pueden no funcionar como se espera. Por favor, descarga la última versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas funciones y correcciones de errores." } }, "maps": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 1a20b3c3..22935a54 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -14,7 +14,12 @@ "refuse": "Refuser", "apply": "Appliquer", "copy": "Copier", - "copied": "Copié !" + "copied": "Copié !", + "confirm": "Confirmer", + "choose-folder": "Choisir un dossier", + "unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯", + "warning": "Attention", + "continue": "Continuer" }, "nav-bar": { "add-version": "Ajouter une version", @@ -736,6 +741,9 @@ }, "launch-as-admin": "Lancer en administateur", "not-remind-me": "Ne plus me rappeler" + }, + "bs-version-outdated": { + "body": "La version {outdatedVersion} est obsolète, et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Veuillez télécharger la dernière version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et corrections de bugs." } }, "maps": { diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 11b3a6bd..5a55106c 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -14,7 +14,12 @@ "refuse": "拒否する", "apply": "適用", "copy": "コピー", - "copied": "コピー済み!" + "copied": "コピー済み!", + "confirm": "確認", + "choose-folder": "フォルダーを選択", + "unknown": "不明なエラーが発生しました (´・ω・`)", + "warning": "警告", + "continue": "続ける" }, "nav-bar": { "add-version": "バージョンを追加", @@ -735,6 +740,9 @@ }, "launch-as-admin": "管理者として起動", "not-remind-me": "Больше не напоминать" + }, + "bs-version-outdated": { + "body": "バージョン {outdatedVersion} は古いため、一部のモッドや機能が期待通りに動作しない場合があります。最新の機能やバグ修正を楽しむために、推奨される最新バージョンの Beat Saber ({recommendedVersion}) をダウンロードしてください。" } }, "maps": { diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index eba895b8..244fdbb8 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -14,7 +14,12 @@ "refuse": "Отказаться", "apply": "Применить", "copy": "Скопировать", - "copied": "Скопировано!" + "copied": "Скопировано!", + "confirm": "Подтвердить", + "choose-folder": "Изменить папку", + "unknown": "Неизвестная ошибка ¯\\_(ツ)_/¯", + "warning": "Внимание", + "continue": "Продолжить" }, "nav-bar": { "add-version": "Добавить версию игры", @@ -734,6 +739,9 @@ "info-3": "Обратите внимание, что не рекомендуется предоставлять права администратора Steam, так как это также влияет на установленные игры и моды, представляя угрозу безопасности." }, "launch-as-admin": "Запустить от имени администратора" + }, + "bs-version-outdated": { + "body": "Версия {outdatedVersion} устарела, и некоторые моды или функции могут работать некорректно. Пожалуйста, скачайте последнюю рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться новейшими функциями и исправлениями ошибок." } }, "maps": { diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 81e0ee64..53ecf7d4 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -14,7 +14,12 @@ "refuse": "拒絕", "apply": "應用", "copy": "複製", - "copied": "已複製!" + "copied": "已複製!", + "confirm": "確認", + "choose-folder": "選擇文件夾", + "unknown": "發生了一個錯誤 ¯\\_(ツ)_/¯", + "warning": "警告", + "continue": "繼續" }, "nav-bar": { "add-version": "新增版本", @@ -735,6 +740,9 @@ }, "launch-as-admin": "以管理員身份啟動", "not-remind-me": "不再提醒我" + }, + "bs-version-outdated": { + "body": "版本 {outdatedVersion} 已過時,某些模組或功能可能無法正常運作。請下載最新推薦的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修正。" } }, "maps": { diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index de4aa7af..c8573197 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -14,7 +14,12 @@ "refuse": "拒绝", "apply": "应用", "copy": "复制", - "copied": "已复制!" + "copied": "已复制!", + "confirm": "确认", + "choose-folder": "选择文件夹", + "Unknown": "发生了一个错误 ¯\\_(ツ)_/¯", + "warning": "警告", + "continue": "继续" }, "nav-bar": { "add-version": "添加版本", @@ -735,6 +740,9 @@ }, "launch-as-admin": "以管理员身份启动", "not-remind-me": "不再提醒我" + }, + "bs-version-outdated": { + "body": "版本 {outdatedVersion} 已过时,某些模组或功能可能无法正常工作。请下载最新推荐的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修复。" } }, "maps": { diff --git a/package-lock.json b/package-lock.json index bb16bf03..bff7c20a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.7", "fast-deep-equal": "^3.1.3", - "framer-motion": "^10.16.16", + "framer-motion": "^11.2.6", "fs-extra": "^11.2.0", "history": "^5.3.0", "is-elevated": "^3.0.0", @@ -1017,6 +1017,7 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", "optional": true, + "peer": true, "dependencies": { "@emotion/memoize": "0.7.4" } @@ -1025,7 +1026,8 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true + "optional": true, + "peer": true }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", @@ -8899,20 +8901,22 @@ } }, "node_modules/framer-motion": { - "version": "10.16.16", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz", - "integrity": "sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==", + "version": "11.11.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.17.tgz", + "integrity": "sha512-O8QzvoKiuzI5HSAHbcYuL6xU+ZLXbrH7C8Akaato4JzQbX2ULNeniqC2Vo5eiCtFktX9XsJ+7nUhxcl2E2IjpA==", + "license": "MIT", "dependencies": { "tslib": "^2.4.0" }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, "peerDependencies": { + "@emotion/is-prop-valid": "*", "react": "^18.0.0", "react-dom": "^18.0.0" }, "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, "react": { "optional": true }, @@ -19631,6 +19635,7 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", "optional": true, + "peer": true, "requires": { "@emotion/memoize": "0.7.4" } @@ -19639,7 +19644,8 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true + "optional": true, + "peer": true }, "@eslint/eslintrc": { "version": "1.3.3", @@ -25602,11 +25608,10 @@ "dev": true }, "framer-motion": { - "version": "10.16.16", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz", - "integrity": "sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==", + "version": "11.11.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.17.tgz", + "integrity": "sha512-O8QzvoKiuzI5HSAHbcYuL6xU+ZLXbrH7C8Akaato4JzQbX2ULNeniqC2Vo5eiCtFktX9XsJ+7nUhxcl2E2IjpA==", "requires": { - "@emotion/is-prop-valid": "^0.8.2", "tslib": "^2.4.0" } }, diff --git a/package.json b/package.json index 6c770953..1a3c5abe 100644 --- a/package.json +++ b/package.json @@ -223,7 +223,7 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.7", "fast-deep-equal": "^3.1.3", - "framer-motion": "^10.16.16", + "framer-motion": "^11.2.6", "fs-extra": "^11.2.0", "history": "^5.3.0", "is-elevated": "^3.0.0", diff --git a/src/renderer/components/modal/modal-types/bs-version-outdated-modal.component.tsx b/src/renderer/components/modal/modal-types/bs-version-outdated-modal.component.tsx new file mode 100644 index 00000000..fd333016 --- /dev/null +++ b/src/renderer/components/modal/modal-types/bs-version-outdated-modal.component.tsx @@ -0,0 +1,42 @@ +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { BsmImage } from "renderer/components/shared/bsm-image.component"; +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; +import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"; +import { BSVersion } from "shared/bs-version.interface"; +import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; +import { useTranslationV2 } from "renderer/hooks/use-translation.hook"; +import { useState } from "react"; +import { ConfigurationService } from "renderer/services/configuration.service"; +import { useService } from "renderer/hooks/use-service.hook"; + +export const BSVersionOutdatedModal: ModalComponent = ({ resolver, data : { outdated, recommended }}) => { + + const t = useTranslationV2(); + const config = useService(ConfigurationService); + + const [remember, setRemember] = useState(false); + + const handleContinue = () => { + if (remember) { + config.set("not-show-bs-version-outdated", true); + } + + resolver({ exitCode: ModalExitCode.COMPLETED }); + }; + + return ( +
+

{t.text("misc.warning")}

+ +

{t.element("modals.bs-version-outdated.body", { outdatedVersion: {outdated.BSVersion}, recommendedVersion: {recommended.BSVersion} })}

+
+ setRemember(() => val)} /> + {t.text("modals.misc.remember-my-choice")} +
+
+ resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" /> + +
+ + ); +}; diff --git a/src/renderer/hooks/use-translation.hook.ts b/src/renderer/hooks/use-translation.hook.ts index 5663835c..d26600d6 100644 --- a/src/renderer/hooks/use-translation.hook.ts +++ b/src/renderer/hooks/use-translation.hook.ts @@ -1,7 +1,12 @@ import { I18nService } from "renderer/services/i18n.service"; import { useService } from "./use-service.hook"; import { useObservable } from "./use-observable.hook"; +import { useConstant } from "./use-constant.hook"; +import { createElement, Fragment } from "react"; +/** + * @deprecated Use useTranslationV2 instead + */ export function useTranslation(): (translationKey: string, args?: Record) => string { const i18nService = useService(I18nService); @@ -16,3 +21,35 @@ export function useTranslation(): (translationKey: string, args?: Record i18nService.translate(key, args)).join(" "); }; } + +type TranslateFunctions = { + text: (translationKey: string, args?: Record) => string; + element: (translationKey: string, args?: Record) => JSX.Element; +} + +export function useTranslationV2(): TranslateFunctions { + + const i18nService = useService(I18nService); + + useObservable(() => i18nService.currentLanguage$); + + const text = useConstant(() => { + return (key: string, args?: Record) => { + if (!key) { + return key; + } + return i18nService.translate(key, args); + }; + }); + + const element = useConstant(() => { + return (key: string, args?: Record) => { + if (!key) { + return createElement(Fragment, null, key); + } + return i18nService.translateElement(key, args); + }; + }); + + return { text, element }; +} diff --git a/src/renderer/pages/available-versions-list.components.tsx b/src/renderer/pages/available-versions-list.components.tsx index eed439a8..93d53753 100644 --- a/src/renderer/pages/available-versions-list.components.tsx +++ b/src/renderer/pages/available-versions-list.components.tsx @@ -11,6 +11,12 @@ import { useService } from "renderer/hooks/use-service.hook"; import { BSVersion } from "shared/bs-version.interface"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service"; +import { logRenderError } from "renderer"; +import { ModalExitCode, ModalService } from "renderer/services/modale.service"; +import { BSVersionOutdatedModal } from "renderer/components/modal/modal-types/bs-version-outdated-modal.component"; +import { ConfigurationService } from "renderer/services/configuration.service"; +import { coerce, lt, valid } from "semver"; +import { tryit } from "shared/helpers/error.helpers"; export const AvailableVersionsContext = createContext<{ selectedVersion: BSVersion; setSelectedVersion: (version: BSVersion) => void }>(null); @@ -18,6 +24,8 @@ export function AvailableVersionsList() { const bsDownloader = useService(BsDownloaderService); const versionManager = useService(BSVersionManagerService); + const modals = useService(ModalService); + const config = useService(ConfigurationService); const [selectedVersion, setSelectedVersion] = useState(null); const contextValue = useMemo(() => ({ selectedVersion, setSelectedVersion }), [selectedVersion]); @@ -25,10 +33,25 @@ export function AvailableVersionsList() { const downloading = useObservable(() => bsDownloader.downloadingVersion$.pipe(map(v => !!v))); const t = useTranslation(); - const startDownload = async () => { + const startDownload = async (version: BSVersion) => { + const showOutdated = !config.get("not-show-bs-version-outdated"); + const recommendedVersion = showOutdated ? versionManager.availableVersions$.value.find(v => v.recommended) : undefined; - return bsDownloader.downloadVersion(selectedVersion) - .catch(console.log) + const isVersionOutdated = recommendedVersion?.BSVersion ? ( + tryit(() => lt(valid(coerce(version.BSVersion)), valid(coerce(recommendedVersion.BSVersion)))).result ?? false + ) : false; + + if (showOutdated && isVersionOutdated) { + + const res = await modals.openModal(BSVersionOutdatedModal, { outdated: version, recommended: recommendedVersion }); + + if (res.exitCode !== ModalExitCode.COMPLETED) { + return; + } + } + + return bsDownloader.downloadVersion(version) + .catch(logRenderError) .finally(() => setSelectedVersion(null)); }; @@ -45,17 +68,17 @@ export function AvailableVersionsList() { return (
- +

{t("pages.available-versions.title")}

- + {selectedVersion && !downloading && ( - + startDownload(selectedVersion)}> )} diff --git a/src/renderer/services/i18n.service.ts b/src/renderer/services/i18n.service.ts index 618103dd..36ee0ee4 100644 --- a/src/renderer/services/i18n.service.ts +++ b/src/renderer/services/i18n.service.ts @@ -4,6 +4,7 @@ import { Observable } from "rxjs"; import { ConfigurationService } from "./configuration.service"; import { getProperty } from "dot-prop"; import { i18n } from "dateformat"; +import { createElement, Fragment } from "react"; export class I18nService { private static instance: I18nService; @@ -87,4 +88,30 @@ export class I18nService { }); return translated; } + + public translateElement(translationKey: string, args?: Record): JSX.Element { + let translated = this.cache.get(translationKey); + if (!translated) { + translated = getProperty(this.dictionary, translationKey) ?? translationKey; + this.cache.set(translationKey, translated); + } + + if (!args) { + return createElement(Fragment, null, translated); + } + + // Sort keys by length in descending order to avoid partial matches within keys + const sortedKeys = Object.keys(args).sort((a, b) => b.length - a.length); + + const pattern = sortedKeys.map(key => `\\{${key}\\}`).join('|'); + const regex = new RegExp(`(${pattern})`, 'g'); + const segments = translated.split(regex); + + const children = segments.map((segment) => { + const key = segment.slice(1, -1); // Remove surrounding {} + return args[key] || segment; + }); + + return createElement(Fragment, null, ...children); + } }