mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
[Feature] Implement internazionalization for Decky Loader (#361)
* First iteration for internationalization of the loader * First iteration for internationalization of the loader * Cleanup node mess * Cleanup node mess pt2 * Additional touches * Latest decky changed merged into i18n and updated translation. * Styling fixes * Initial backend hosting implementation * Added correct url path of the loopback server. * Added correct url path of the loopback server. * Some better namespaced text. * Added whitelist for locales path. * Refactor languages and fix hooks logic bugs. * Small typo in language translation structure. * Working backend, automatically swtich languages with steam and language fixes. * Fix to languages * Key fixes * Additional language fixes. * Additional json changes * Final text revision and added a vscode tasks to automatically extract text from code. * Typo in the middleware * Remove unused imports * Cleanup whitespaces. * Import changes * Revert "Import changes" This reverts commit8e8231950f. * Update index.d.ts * Clean up unused imports * Delete pnpm-lock.yaml * Update rollup.config.js * Update PluginInstallModal.tsx * Update index.tsx * Update plugin-loader.tsx * Update plugin-loader.tsx * Revert "Delete pnpm-lock.yaml" This reverts commit3a39f36f21. * Additional strings reworks. * Fixes for issues coming from github merge. * Fixes for master * Styling fixes * Styling pt2 * Missed a few strings in master, * Styling fixes * Additional master merge fixes. * Final cleanup and adaptation to master. * Final empty language cleanup and few string added * Small changes to italian translation * Disabled translation on a few components inside plugin-loader for missing react hooks. * Fixed passing tag to translation. * Disable debug output for reducing console spam. * Return correct content type * Small italian language change * Added support for country code * Fixed missing translation for uninstall popup. * Fix class name shenanigans for toast notification * Update dependencies * Fixed github workflow to include the new locales folder * Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up * Missed a file name change * Updated dev dependencies to latest version * Missed a few dev dependencies * Revert "Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up" Messed up merge with a different main branch * Messed up deletion of rollup config. * Fix broken pnpm lock file * Missed a localized string during the merge * Fixed a parameter mistake in the uninstall text parameter * Fix pnpm random issues * Small italian language tweaks * Fix wrong parameter passed to the uninstall function call * Another fix on a wrong function parameter * Additional translation text on the store and branch selection channels * Changed the default type passed to map to being able to index the two arrays. * Reverted and reworked the last changes * Distinguish events in UI for installing vs reinstalling plugins * Additional fixes for reinstall prompt * Revert the use of intevalPlural since the parser doesn't seem to support that. * Missed a routing path in the backend * Small bugfixes * Small fixes * Correctly adding the parameter to the request headers. * Refactoring of the UI popup modal * Fix pnpm shenanigans * Final fixes for the install UI localization * Clean up unnedeed backend code * Small rework on text selection. * Cleaned up parser configuration * Removed extracttext dependency to pnpmsetup * Merged translation and cleaned up parser * Fixed JSON structure after manual merge. * Added translation to the file picker * Revert changes to PluginInstallModal * Reworked the text modal for the final time * Missed the proper linted text * Missed the backend change * Final branch cleanup * Fixed small translation bleeding Caused from the manual merge of _old.json files. * fix extra space in browser.py * fix extra newline in plugin-loader.tsx * Cleanup i18next-parser.config.mjs * Update plugin-loader.tsx * Cleanup language files * Better labeling of text * Fixed language typos in BranchSelect * Fixed language typos in StoreSelect * Cleanup plugin-loader.tsx from unused imports * Removed the path bypass since I'm using authentication from the frontend. * Reimplemented this component as a functional component. * Updated dependencies and lockfile * Removed static route from main.py Already handled in loader.py * Small italian coherency fixes * Fix small typography fixes on plugin name uninstall * Fixed italian typo on removal popup * Reenabled manual escaping value in i18next * Set to fallback to the default language if the string in the JSON file is empty. * Fixed pnpm wankery * Added a missed italian text translation string --------- Co-authored-by: AAGaming <aa@mail.catvibers.me>
This commit is contained in:
@@ -1,18 +1,31 @@
|
||||
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import TPluginInstallModal, { TranslatedPart } from './TPluginInstallModal';
|
||||
|
||||
interface PluginInstallModalProps {
|
||||
artifact: string;
|
||||
version: string;
|
||||
hash: string;
|
||||
// reinstall: boolean;
|
||||
installType: number;
|
||||
onOK(): void;
|
||||
onCancel(): void;
|
||||
closeModal?(): void;
|
||||
}
|
||||
|
||||
const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, hash, onOK, onCancel, closeModal }) => {
|
||||
const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
artifact,
|
||||
version,
|
||||
hash,
|
||||
installType,
|
||||
onOK,
|
||||
onCancel,
|
||||
closeModal,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
bOKDisabled={loading}
|
||||
@@ -26,14 +39,22 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
}}
|
||||
strTitle={`Install ${artifact}`}
|
||||
strOKButtonText={loading ? 'Installing' : 'Install'}
|
||||
strTitle={<TPluginInstallModal trans_part={TranslatedPart.TITLE} trans_type={installType} artifact={artifact} />}
|
||||
strOKButtonText={
|
||||
loading ? (
|
||||
<TPluginInstallModal trans_part={TranslatedPart.BUTTON_PROC} trans_type={installType} />
|
||||
) : (
|
||||
<TPluginInstallModal trans_part={TranslatedPart.BUTTON_IDLE} trans_type={installType} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Are you sure you want to install {artifact}
|
||||
{version ? ` ${version}` : ''}?
|
||||
{hash == 'False' && (
|
||||
<span style={{ color: 'red' }}> This plugin does not have a hash, you are installing it at your own risk.</span>
|
||||
)}
|
||||
<TPluginInstallModal
|
||||
trans_part={TranslatedPart.DESC}
|
||||
trans_type={installType}
|
||||
artifact={artifact}
|
||||
version={version ? version : ''}
|
||||
/>
|
||||
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { FC } from 'react';
|
||||
import { Translation } from 'react-i18next';
|
||||
|
||||
import { InstallType } from '../../plugin';
|
||||
|
||||
export enum TranslatedPart {
|
||||
TITLE,
|
||||
DESC,
|
||||
BUTTON_IDLE,
|
||||
BUTTON_PROC,
|
||||
}
|
||||
interface TPluginInstallModalProps {
|
||||
trans_part: TranslatedPart;
|
||||
trans_type: number;
|
||||
artifact?: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
const TPluginInstallModal: FC<TPluginInstallModalProps> = ({ trans_part, trans_type, artifact, version }) => {
|
||||
return (
|
||||
<Translation>
|
||||
{(t, {}) => {
|
||||
switch (trans_part) {
|
||||
case TranslatedPart.TITLE:
|
||||
switch (trans_type) {
|
||||
case InstallType.INSTALL:
|
||||
return <div>{t('PluginInstallModal.install.title', { artifact: artifact })}</div>;
|
||||
case InstallType.REINSTALL:
|
||||
return <div>{t('PluginInstallModal.reinstall.title', { artifact: artifact })}</div>;
|
||||
case InstallType.UPDATE:
|
||||
return <div>{t('PluginInstallModal.update.title', { artifact: artifact })}</div>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
case TranslatedPart.DESC:
|
||||
switch (trans_type) {
|
||||
case InstallType.INSTALL:
|
||||
return (
|
||||
<div>
|
||||
{t('PluginInstallModal.install.desc', {
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
case InstallType.REINSTALL:
|
||||
return (
|
||||
<div>
|
||||
{t('PluginInstallModal.reinstall.desc', {
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
case InstallType.UPDATE:
|
||||
return (
|
||||
<div>
|
||||
{t('PluginInstallModal.update.desc', {
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
case TranslatedPart.BUTTON_IDLE:
|
||||
switch (trans_type) {
|
||||
case InstallType.INSTALL:
|
||||
return <div>{t('PluginInstallModal.install.button_idle')}</div>;
|
||||
case InstallType.REINSTALL:
|
||||
return <div>{t('PluginInstallModal.reinstall.button_idle')}</div>;
|
||||
case InstallType.UPDATE:
|
||||
return <div>{t('PluginInstallModal.update.button_idle')}</div>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
case TranslatedPart.BUTTON_PROC:
|
||||
switch (trans_type) {
|
||||
case InstallType.INSTALL:
|
||||
return <div>{t('PluginInstallModal.install.button_processing')}</div>;
|
||||
case InstallType.REINSTALL:
|
||||
return <div>{t('PluginInstallModal.reinstall.button_processing')}</div>;
|
||||
case InstallType.UPDATE:
|
||||
return <div>{t('PluginInstallModal.update.button_processing')}</div>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}}
|
||||
</Translation>
|
||||
);
|
||||
};
|
||||
|
||||
export default TPluginInstallModal;
|
||||
@@ -2,6 +2,7 @@ import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend
|
||||
import { useEffect } from 'react';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { FileIcon, defaultStyles } from 'react-file-icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaArrowUp, FaFolder } from 'react-icons/fa';
|
||||
|
||||
import Logger from '../../../logger';
|
||||
@@ -47,6 +48,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
onSubmit,
|
||||
closeModal,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
|
||||
const [path, setPath] = useState<string>(startPath);
|
||||
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
|
||||
@@ -158,7 +160,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
closeModal?.();
|
||||
}}
|
||||
>
|
||||
Use this folder
|
||||
{t('FilePickerIndex.folder.select')}
|
||||
</DialogButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCode, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../utils/hooks/useSetting';
|
||||
@@ -12,22 +13,23 @@ const DeveloperSettings = lazy(() => import('./pages/developer'));
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: 'Decky',
|
||||
title: t('SettingsIndex.general_title'),
|
||||
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
|
||||
route: '/decky/settings/general',
|
||||
icon: <DeckyIcon />,
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
title: t('SettingsIndex.plugins_title'),
|
||||
content: <PluginList />,
|
||||
route: '/decky/settings/plugins',
|
||||
icon: <FaPlug />,
|
||||
},
|
||||
{
|
||||
title: 'Developer',
|
||||
title: t('SettingsIndex.developer_title'),
|
||||
content: (
|
||||
<WithSuspense>
|
||||
<DeveloperSettings />
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol } from 'react-icons/fa';
|
||||
|
||||
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
||||
@@ -24,8 +25,10 @@ const installFromZip = () => {
|
||||
installFromURL(url);
|
||||
} else {
|
||||
window.DeckyPluginLoader.toaster.toast({
|
||||
//title: t('SettingsDeveloperIndex.toast_zip.title'),
|
||||
title: 'Decky',
|
||||
body: `Installation failed! Only ZIP files are supported.`,
|
||||
//body: t('SettingsDeveloperIndex.toast_zip.body'),
|
||||
body: 'Installation failed! Only ZIP files are supported.',
|
||||
onClick: installFromZip,
|
||||
});
|
||||
}
|
||||
@@ -38,33 +41,47 @@ export default function DeveloperSettings() {
|
||||
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Third-Party Plugins</DialogControlsSectionHeader>
|
||||
<Field label="Install Plugin from ZIP File" icon={<FaFileArchive style={{ display: 'block' }} />}>
|
||||
<DialogButton onClick={installFromZip}>Browse</DialogButton>
|
||||
<DialogControlsSectionHeader>
|
||||
{t('SettingsDeveloperIndex.third_party_plugins.header')}
|
||||
</DialogControlsSectionHeader>
|
||||
<Field
|
||||
label={t('SettingsDeveloperIndex.third_party_plugins.label_zip')}
|
||||
icon={<FaFileArchive style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton onClick={installFromZip}>
|
||||
{t('SettingsDeveloperIndex.third_party_plugins.button_zip')}
|
||||
</DialogButton>
|
||||
</Field>
|
||||
<Field
|
||||
label="Install Plugin from URL"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
label={t('SettingsDeveloperIndex.third_party_plugins.label_url')}
|
||||
description={
|
||||
<TextField
|
||||
label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
|
||||
value={pluginURL}
|
||||
onChange={(e) => setPluginURL(e?.target.value)}
|
||||
/>
|
||||
}
|
||||
icon={<FaLink style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||
<DialogControlsSectionHeader>{t('SettingsDeveloperIndex.header_other')}</DialogControlsSectionHeader>
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Enable Valve Internal"
|
||||
label={t('SettingsDeveloperIndex.valve_internal.label')}
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables the Valve internal developer menu.{' '}
|
||||
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
|
||||
{t('SettingsDeveloperIndex.valve_internal.desc1')}{' '}
|
||||
<span style={{ color: 'red' }}>{t('SettingsDeveloperIndex.valve_internal.desc2')}</span>
|
||||
</span>
|
||||
}
|
||||
icon={<FaSteamSymbol style={{ display: 'block' }} />}
|
||||
@@ -78,17 +95,18 @@ export default function DeveloperSettings() {
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
label={t('SettingsDeveloperIndex.react_devtools.label')}
|
||||
description={
|
||||
<>
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set
|
||||
the IP address before enabling.
|
||||
</span>
|
||||
<span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
|
||||
<br />
|
||||
<br />
|
||||
<div ref={textRef}>
|
||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
||||
<TextField
|
||||
label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
|
||||
value={reactDevtoolsIP}
|
||||
onChange={(e) => setReactDevtoolsIP(e?.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dropdown, Field } from 'decky-frontend-lib';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Logger from '../../../../logger';
|
||||
import { callUpdaterMethod } from '../../../../updater';
|
||||
@@ -14,17 +15,23 @@ enum UpdateBranch {
|
||||
}
|
||||
|
||||
const BranchSelect: FunctionComponent<{}> = () => {
|
||||
const { t } = useTranslation();
|
||||
const tBranches = [
|
||||
t('BranchSelect.update_channel.stable'),
|
||||
t('BranchSelect.update_channel.prerelease'),
|
||||
t('BranchSelect.update_channel.testing'),
|
||||
];
|
||||
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Prerelease);
|
||||
|
||||
return (
|
||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
||||
// 0 being stable, 1 being pre-release and 2 being nightly
|
||||
<Field label="Decky Update Channel" childrenContainerWidth={'fixed'}>
|
||||
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(UpdateBranch)
|
||||
.filter((branch) => typeof branch == 'string')
|
||||
.map((branch) => ({
|
||||
label: branch,
|
||||
label: tBranches[UpdateBranch[branch]],
|
||||
data: UpdateBranch[branch],
|
||||
}))}
|
||||
selectedOption={selectedBranch}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaChrome } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
|
||||
export default function RemoteDebuggingSettings() {
|
||||
const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Field
|
||||
label="Allow Remote CEF Debugging"
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Allows unauthenticated access to the CEF debugger to anyone in your network.
|
||||
</span>
|
||||
}
|
||||
label={t('RemoteDebugging.remote_cef.label')}
|
||||
description={<span style={{ whiteSpace: 'pre-line' }}>{t('RemoteDebugging.remote_cef.desc')}</span>}
|
||||
icon={<FaChrome style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dropdown, Field, TextField } from 'decky-frontend-lib';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaShapes } from 'react-icons/fa';
|
||||
|
||||
import Logger from '../../../../logger';
|
||||
@@ -11,17 +12,23 @@ const logger = new Logger('StoreSelect');
|
||||
const StoreSelect: FunctionComponent<{}> = () => {
|
||||
const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default);
|
||||
const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null);
|
||||
const { t } = useTranslation();
|
||||
const tStores = [
|
||||
t('StoreSelect.store_channel.default'),
|
||||
t('StoreSelect.store_channel.testing'),
|
||||
t('StoreSelect.store_channel.custom'),
|
||||
];
|
||||
|
||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
||||
// 0 being Default, 1 being Testing and 2 being Custom
|
||||
return (
|
||||
<>
|
||||
<Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}>
|
||||
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(Store)
|
||||
.filter((store) => typeof store == 'string')
|
||||
.map((store) => ({
|
||||
label: store,
|
||||
label: tStores[Store[store]],
|
||||
data: Store[store],
|
||||
}))}
|
||||
selectedOption={selectedStore}
|
||||
@@ -33,11 +40,11 @@ const StoreSelect: FunctionComponent<{}> = () => {
|
||||
</Field>
|
||||
{selectedStore == Store.Custom && (
|
||||
<Field
|
||||
label="Custom Store"
|
||||
label={t('StoreSelect.custom_store.label')}
|
||||
indentLevel={1}
|
||||
description={
|
||||
<TextField
|
||||
label={'URL'}
|
||||
label={t('StoreSelect.custom_store.url_label')}
|
||||
value={selectedStoreURL || undefined}
|
||||
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useCallback } from 'react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||
@@ -23,6 +24,7 @@ const MarkdownRenderer = lazy(() => import('../../../Markdown'));
|
||||
|
||||
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
|
||||
const SP = findSP();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Focusable onCancelButton={closeModal}>
|
||||
<FocusRing>
|
||||
@@ -45,7 +47,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
|
||||
</WithSuspense>
|
||||
) : (
|
||||
'no patch notes for this version'
|
||||
t('Updater.no_patch_notes_desc')
|
||||
)}
|
||||
</div>
|
||||
</Focusable>
|
||||
@@ -58,7 +60,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
initialColumn={0}
|
||||
autoFocus={true}
|
||||
fnGetColumnWidth={() => SP.innerWidth}
|
||||
name="Decky Updates"
|
||||
name={t('Updater.decky_updates') as string}
|
||||
/>
|
||||
</FocusRing>
|
||||
</Focusable>
|
||||
@@ -72,6 +74,8 @@ export default function UpdaterSettings() {
|
||||
const [updateProgress, setUpdateProgress] = useState<number>(-1);
|
||||
const [reloading, setReloading] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
window.DeckyUpdater = {
|
||||
updateProgress: (i) => {
|
||||
@@ -93,14 +97,14 @@ export default function UpdaterSettings() {
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
|
||||
onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
|
||||
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
|
||||
label="Decky Updates"
|
||||
label={t('Updater.updates.label')}
|
||||
description={
|
||||
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
|
||||
''
|
||||
) : (
|
||||
<span>Up to date: running {versionInfo?.current}</span>
|
||||
<span>{t('Updater.updates.lat_version', { ver: versionInfo?.current })} </span>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
@@ -129,10 +133,10 @@ export default function UpdaterSettings() {
|
||||
}
|
||||
>
|
||||
{checkingForUpdates
|
||||
? 'Checking'
|
||||
? t('Updater.updates.checking')
|
||||
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
|
||||
? 'Check For Updates'
|
||||
: 'Install Update'}
|
||||
? t('Updater.updates.check_button')
|
||||
: t('Updater.updates.install_button')}
|
||||
</DialogButton>
|
||||
) : (
|
||||
<ProgressBarWithInfo
|
||||
@@ -140,7 +144,7 @@ export default function UpdaterSettings() {
|
||||
bottomSeparator="none"
|
||||
nProgress={updateProgress}
|
||||
indeterminate={reloading}
|
||||
sOperationText={reloading ? 'Reloading' : 'Updating'}
|
||||
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import BranchSelect from './BranchSelect';
|
||||
@@ -13,21 +14,22 @@ export default function GeneralSettings({
|
||||
setIsDeveloper: (val: boolean) => void;
|
||||
}) {
|
||||
const { versionInfo } = useDeckyState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.updates.header')}</DialogControlsSectionHeader>
|
||||
<UpdaterSettings />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.beta.header')}</DialogControlsSectionHeader>
|
||||
<BranchSelect />
|
||||
<StoreSelect />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||
<Field label="Enable Developer Mode">
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
|
||||
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
|
||||
<Toggle
|
||||
value={isDeveloper}
|
||||
onChange={(toggleValue) => {
|
||||
@@ -37,8 +39,8 @@ export default function GeneralSettings({
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
|
||||
<Field label="Decky Version" focusable={true}>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.about.header')}</DialogControlsSectionHeader>
|
||||
<Field label={t('SettingsGeneralIndex.about.decky_version')} focusable={true}>
|
||||
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
showContextMenu,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
||||
|
||||
import { InstallType } from '../../../../plugin';
|
||||
import { StorePluginVersion, getPluginList, requestPluginInstall } from '../../../../store';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
@@ -25,19 +27,33 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
|
||||
const remotePlugin = serverData?.find((x) => x.name == pluginName);
|
||||
if (remotePlugin && remotePlugin.versions?.length > 0) {
|
||||
const currentVersionData = remotePlugin.versions.find((version) => version.name == currentVersion);
|
||||
if (currentVersionData) requestPluginInstall(pluginName, currentVersionData);
|
||||
if (currentVersionData) requestPluginInstall(pluginName, currentVersionData, InstallType.REINSTALL);
|
||||
}
|
||||
}
|
||||
|
||||
function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
||||
const data = props.entry.data;
|
||||
const { t } = useTranslation();
|
||||
let pluginName = labelToName(props.entry.label, data?.version);
|
||||
|
||||
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
|
||||
showContextMenu(
|
||||
<Menu label="Plugin Actions">
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}>Reload</MenuItem>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(pluginName)}>Uninstall</MenuItem>
|
||||
<Menu label={t('PluginListIndex.plugin_actions')}>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}>
|
||||
{t('PluginListIndex.reload')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelected={() =>
|
||||
window.DeckyPluginLoader.uninstallPlugin(
|
||||
pluginName,
|
||||
t('PluginLoader.plugin_uninstall.title', { name: pluginName }),
|
||||
t('PluginLoader.plugin_uninstall.button'),
|
||||
t('PluginLoader.plugin_uninstall.desc', { name: pluginName }),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('PluginListIndex.uninstall')}
|
||||
</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
);
|
||||
@@ -48,11 +64,11 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
||||
{data?.update ? (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion)}
|
||||
onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion)}
|
||||
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
|
||||
onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Update to {data?.update?.name}
|
||||
{t('PluginListIndex.update_to', { name: data?.update?.name })}
|
||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
@@ -63,7 +79,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
||||
onOKButton={() => reinstallPlugin(pluginName, data?.version)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Reinstall
|
||||
{t('PluginListIndex.reinstall')}
|
||||
<FaRecycle style={{ paddingLeft: '5.3rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
@@ -90,6 +106,7 @@ export default function PluginList() {
|
||||
'pluginOrder',
|
||||
plugins.map((plugin) => plugin.name),
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
@@ -115,7 +132,7 @@ export default function PluginList() {
|
||||
if (plugins.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<p>No plugins installed</p>
|
||||
<p>{t('PluginListIndex.no_plugin')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
SuspensefulImage,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { InstallType } from '../../plugin';
|
||||
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
|
||||
|
||||
interface PluginCardProps {
|
||||
@@ -18,6 +20,8 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
const [selectedOption, setSelectedOption] = useState<number>(0);
|
||||
const root: boolean = plugin.tags.some((tag) => tag === 'root');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="deckyStoreCard"
|
||||
@@ -97,7 +101,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
plugin.description
|
||||
) : (
|
||||
<span>
|
||||
<i style={{ color: '#666' }}>No description provided.</i>
|
||||
<i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -109,7 +113,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
color: '#fee75c',
|
||||
}}
|
||||
>
|
||||
<i>This plugin has full access to your Steam Deck.</i>{' '}
|
||||
<i>{t('PluginCard.plugin_full_access')}</i>{' '}
|
||||
<a
|
||||
className="deckyStoreCardDescriptionRootLink"
|
||||
href="https://deckbrew.xyz/root"
|
||||
@@ -144,9 +148,11 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
<ButtonItem
|
||||
bottomSeparator="none"
|
||||
layout="below"
|
||||
onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
|
||||
onClick={() =>
|
||||
requestPluginInstall(plugin.name, plugin.versions[selectedOption], InstallType.INSTALL)
|
||||
}
|
||||
>
|
||||
<span className="deckyStoreCardInstallText">Install</span>
|
||||
<span className="deckyStoreCardInstallText">{t('PluginCard.plugin_install')}</span>
|
||||
</ButtonItem>
|
||||
</div>
|
||||
<div
|
||||
@@ -163,7 +169,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
label: version.name,
|
||||
})) as SingleDropdownOption[]
|
||||
}
|
||||
menuLabel="Plugin Version"
|
||||
menuLabel={t('PluginCard.plugin_version_label') as string}
|
||||
selectedOption={selectedOption}
|
||||
onChange={({ data }) => setSelectedOption(data)}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
findModule,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import logo from '../../../assets/plugin_store.png';
|
||||
import Logger from '../../logger';
|
||||
@@ -25,6 +26,8 @@ const StorePage: FC<{}> = () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await getPluginList();
|
||||
@@ -54,13 +57,13 @@ const StorePage: FC<{}> = () => {
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Browse',
|
||||
title: t('Store.store_tabs.title'),
|
||||
content: <BrowseTab children={{ data: data }} />,
|
||||
id: 'browse',
|
||||
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
title: t('Store.store_tabs.about'),
|
||||
content: <AboutTab />,
|
||||
id: 'about',
|
||||
},
|
||||
@@ -73,10 +76,12 @@ const StorePage: FC<{}> = () => {
|
||||
};
|
||||
|
||||
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortOptions = useMemo(
|
||||
(): DropdownOption[] => [
|
||||
{ data: 1, label: 'Alphabetical (A to Z)' },
|
||||
{ data: 2, label: 'Alphabetical (Z to A)' },
|
||||
{ data: 1, label: t('Store.store_tabs.alph_desc') },
|
||||
{ data: 2, label: t('Store.store_tabs.alph_asce') },
|
||||
],
|
||||
[],
|
||||
);
|
||||
@@ -105,11 +110,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
width: '47.5%',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">Sort</span>
|
||||
<span className="DialogLabel">{t("Store.store_sort.label")}</span>
|
||||
<Dropdown
|
||||
menuLabel="Sort"
|
||||
menuLabel={t("Store.store_sort.label") as string}
|
||||
rgOptions={sortOptions}
|
||||
strDefaultLabel="Last Updated (Newest)"
|
||||
strDefaultLabel={t("Store.store_sort.label_def") as string}
|
||||
selectedOption={selectedSort}
|
||||
onChange={(e) => setSort(e.data)}
|
||||
/>
|
||||
@@ -122,11 +127,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">Filter</span>
|
||||
<span className="DialogLabel">{t("Store.store_filter.label")}</span>
|
||||
<Dropdown
|
||||
menuLabel="Filter"
|
||||
menuLabel={t("Store.store_filter.label")}
|
||||
rgOptions={filterOptions}
|
||||
strDefaultLabel="All"
|
||||
strDefaultLabel={t("Store.store_filter.label_def")}
|
||||
selectedOption={selectedFilter}
|
||||
onChange={(e) => setFilter(e.data)}
|
||||
/>
|
||||
@@ -136,7 +141,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
<div style={{ justifyContent: 'center', display: 'flex' }}>
|
||||
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
|
||||
<TextField label={t("Store.store_search.label")} value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
@@ -151,11 +156,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">Sort</span>
|
||||
<span className="DialogLabel">{t('Store.store_sort.label')}</span>
|
||||
<Dropdown
|
||||
menuLabel="Sort"
|
||||
menuLabel={t('Store.store_sort.label') as string}
|
||||
rgOptions={sortOptions}
|
||||
strDefaultLabel="Last Updated (Newest)"
|
||||
strDefaultLabel={t('Store.store_sort.label_def') as string}
|
||||
selectedOption={selectedSort}
|
||||
onChange={(e) => setSort(e.data)}
|
||||
/>
|
||||
@@ -165,7 +170,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
<div style={{ justifyContent: 'center', display: 'flex' }}>
|
||||
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
|
||||
<TextField
|
||||
label={t('Store.store_search.label')}
|
||||
value={searchFieldValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
@@ -192,6 +201,8 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
};
|
||||
|
||||
const AboutTab: FC<{}> = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -216,7 +227,7 @@ const AboutTab: FC<{}> = () => {
|
||||
/>
|
||||
<span className="deckyStoreAboutHeader">Testing</span>
|
||||
<span>
|
||||
Please consider testing new plugins to help the Decky Loader team!{' '}
|
||||
{t('Store.store_testing_cta')}{' '}
|
||||
<a
|
||||
href="https://deckbrew.xyz/testing"
|
||||
target="_blank"
|
||||
@@ -227,13 +238,10 @@ const AboutTab: FC<{}> = () => {
|
||||
deckbrew.xyz/testing
|
||||
</a>
|
||||
</span>
|
||||
<span className="deckyStoreAboutHeader">Contributing</span>
|
||||
<span>
|
||||
If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template
|
||||
repository on GitHub. Information on development and distribution is available in the README.
|
||||
</span>
|
||||
<span className="deckyStoreAboutHeader">Source Code</span>
|
||||
<span>All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.</span>
|
||||
<span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
|
||||
<span>{t('Store.store_contrib.desc')}</span>
|
||||
<span className="deckyStoreAboutHeader">{t('Store.store_source.label')}</span>
|
||||
<span>{t('Store.store_source.desc')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user