From 35e7c80835866575ea1d8f725f8c07183753e49b Mon Sep 17 00:00:00 2001 From: Marco Rodolfi Date: Tue, 2 May 2023 17:42:39 +0200 Subject: [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 commit 8e8231950fd7cc6cece87040e326d0a72ba79567. * 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 commit 3a39f36f2193cc976d36ffe07338239e363d5b04. * 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 --- .../src/components/modals/PluginInstallModal.tsx | 39 +++++++-- .../src/components/modals/TPluginInstallModal.tsx | 95 ++++++++++++++++++++++ .../src/components/modals/filepicker/index.tsx | 4 +- frontend/src/components/settings/index.tsx | 8 +- .../components/settings/pages/developer/index.tsx | 52 ++++++++---- .../settings/pages/general/BranchSelect.tsx | 11 ++- .../settings/pages/general/RemoteDebugging.tsx | 10 +-- .../settings/pages/general/StoreSelect.tsx | 15 +++- .../components/settings/pages/general/Updater.tsx | 22 +++-- .../components/settings/pages/general/index.tsx | 14 ++-- .../settings/pages/plugin_list/index.tsx | 35 ++++++-- frontend/src/components/store/PluginCard.tsx | 16 ++-- frontend/src/components/store/Store.tsx | 54 ++++++------ frontend/src/developer.tsx | 7 +- frontend/src/index.tsx | 34 +++++++- frontend/src/plugin-loader.tsx | 38 +++++++-- frontend/src/plugin.ts | 6 ++ frontend/src/store.tsx | 5 +- 18 files changed, 360 insertions(+), 105 deletions(-) create mode 100644 frontend/src/components/modals/TPluginInstallModal.tsx (limited to 'frontend/src') diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx index 7f0683ee..0e8e3d47 100644 --- a/frontend/src/components/modals/PluginInstallModal.tsx +++ b/frontend/src/components/modals/PluginInstallModal.tsx @@ -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 = ({ artifact, version, hash, onOK, onCancel, closeModal }) => { +const PluginInstallModal: FC = ({ + artifact, + version, + hash, + installType, + onOK, + onCancel, + closeModal, +}) => { const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + return ( = ({ artifact, version, ha onCancel={async () => { await onCancel(); }} - strTitle={`Install ${artifact}`} - strOKButtonText={loading ? 'Installing' : 'Install'} + strTitle={} + strOKButtonText={ + loading ? ( + + ) : ( + + ) + } > - Are you sure you want to install {artifact} - {version ? ` ${version}` : ''}? - {hash == 'False' && ( - This plugin does not have a hash, you are installing it at your own risk. - )} + + {hash == 'False' && {t('PluginInstallModal.no_hash')}} ); }; diff --git a/frontend/src/components/modals/TPluginInstallModal.tsx b/frontend/src/components/modals/TPluginInstallModal.tsx new file mode 100644 index 00000000..3866560e --- /dev/null +++ b/frontend/src/components/modals/TPluginInstallModal.tsx @@ -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 = ({ trans_part, trans_type, artifact, version }) => { + return ( + + {(t, {}) => { + switch (trans_part) { + case TranslatedPart.TITLE: + switch (trans_type) { + case InstallType.INSTALL: + return
{t('PluginInstallModal.install.title', { artifact: artifact })}
; + case InstallType.REINSTALL: + return
{t('PluginInstallModal.reinstall.title', { artifact: artifact })}
; + case InstallType.UPDATE: + return
{t('PluginInstallModal.update.title', { artifact: artifact })}
; + default: + return null; + } + case TranslatedPart.DESC: + switch (trans_type) { + case InstallType.INSTALL: + return ( +
+ {t('PluginInstallModal.install.desc', { + artifact: artifact, + version: version, + })} +
+ ); + case InstallType.REINSTALL: + return ( +
+ {t('PluginInstallModal.reinstall.desc', { + artifact: artifact, + version: version, + })} +
+ ); + case InstallType.UPDATE: + return ( +
+ {t('PluginInstallModal.update.desc', { + artifact: artifact, + version: version, + })} +
+ ); + default: + return null; + } + case TranslatedPart.BUTTON_IDLE: + switch (trans_type) { + case InstallType.INSTALL: + return
{t('PluginInstallModal.install.button_idle')}
; + case InstallType.REINSTALL: + return
{t('PluginInstallModal.reinstall.button_idle')}
; + case InstallType.UPDATE: + return
{t('PluginInstallModal.update.button_idle')}
; + default: + return null; + } + case TranslatedPart.BUTTON_PROC: + switch (trans_type) { + case InstallType.INSTALL: + return
{t('PluginInstallModal.install.button_processing')}
; + case InstallType.REINSTALL: + return
{t('PluginInstallModal.reinstall.button_processing')}
; + case InstallType.UPDATE: + return
{t('PluginInstallModal.update.button_processing')}
; + default: + return null; + } + } + }} +
+ ); +}; + +export default TPluginInstallModal; diff --git a/frontend/src/components/modals/filepicker/index.tsx b/frontend/src/components/modals/filepicker/index.tsx index ec3fc260..629f4ec5 100644 --- a/frontend/src/components/modals/filepicker/index.tsx +++ b/frontend/src/components/modals/filepicker/index.tsx @@ -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 = ({ onSubmit, closeModal, }) => { + const { t } = useTranslation(); if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path const [path, setPath] = useState(startPath); const [listing, setListing] = useState({ files: [], realpath: path }); @@ -158,7 +160,7 @@ const FilePicker: FunctionComponent = ({ closeModal?.(); }} > - Use this folder + {t('FilePickerIndex.folder.select')} )} diff --git a/frontend/src/components/settings/index.tsx b/frontend/src/components/settings/index.tsx index 6f104710..f3a76407 100644 --- a/frontend/src/components/settings/index.tsx +++ b/frontend/src/components/settings/index.tsx @@ -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('developer.enabled', false); + const { t } = useTranslation(); const pages = [ { - title: 'Decky', + title: t('SettingsIndex.general_title'), content: , route: '/decky/settings/general', icon: , }, { - title: 'Plugins', + title: t('SettingsIndex.plugins_title'), content: , route: '/decky/settings/plugins', icon: , }, { - title: 'Developer', + title: t('SettingsIndex.developer_title'), content: ( diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx index e6e37813..7a62c052 100644 --- a/frontend/src/components/settings/pages/developer/index.tsx +++ b/frontend/src/components/settings/pages/developer/index.tsx @@ -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('developer.rdt.ip', ''); const [pluginURL, setPluginURL] = useState(''); const textRef = useRef(null); + const { t } = useTranslation(); return ( - Third-Party Plugins - }> - Browse + + {t('SettingsDeveloperIndex.third_party_plugins.header')} + + } + > + + {t('SettingsDeveloperIndex.third_party_plugins.button_zip')} + setPluginURL(e?.target.value)} />} + label={t('SettingsDeveloperIndex.third_party_plugins.label_url')} + description={ + setPluginURL(e?.target.value)} + /> + } icon={} > installFromURL(pluginURL)}> - Install + {t('SettingsDeveloperIndex.third_party_plugins.button_install')} - Other + {t('SettingsDeveloperIndex.header_other')} - Enables the Valve internal developer menu.{' '} - Do not touch anything in this menu unless you know what it does. + {t('SettingsDeveloperIndex.valve_internal.desc1')}{' '} + {t('SettingsDeveloperIndex.valve_internal.desc2')} } icon={} @@ -78,17 +95,18 @@ export default function DeveloperSettings() { /> - - Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set - the IP address before enabling. - + {t('SettingsDeveloperIndex.react_devtools.desc')}

- setReactDevtoolsIP(e?.target.value)} /> + setReactDevtoolsIP(e?.target.value)} + />
} diff --git a/frontend/src/components/settings/pages/general/BranchSelect.tsx b/frontend/src/components/settings/pages/general/BranchSelect.tsx index 5387b655..d966ff62 100644 --- a/frontend/src/components/settings/pages/general/BranchSelect.tsx +++ b/frontend/src/components/settings/pages/general/BranchSelect.tsx @@ -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('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 - + typeof branch == 'string') .map((branch) => ({ - label: branch, + label: tBranches[UpdateBranch[branch]], data: UpdateBranch[branch], }))} selectedOption={selectedBranch} diff --git a/frontend/src/components/settings/pages/general/RemoteDebugging.tsx b/frontend/src/components/settings/pages/general/RemoteDebugging.tsx index db604c69..60d57d91 100644 --- a/frontend/src/components/settings/pages/general/RemoteDebugging.tsx +++ b/frontend/src/components/settings/pages/general/RemoteDebugging.tsx @@ -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('cef_forward', false); + const { t } = useTranslation(); return ( - Allows unauthenticated access to the CEF debugger to anyone in your network. - - } + label={t('RemoteDebugging.remote_cef.label')} + description={{t('RemoteDebugging.remote_cef.desc')}} icon={} > = () => { const [selectedStore, setSelectedStore] = useSetting('store', Store.Default); const [selectedStoreURL, setSelectedStoreURL] = useSetting('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 ( <> - + typeof store == 'string') .map((store) => ({ - label: store, + label: tStores[Store[store]], data: Store[store], }))} selectedOption={selectedStore} @@ -33,11 +40,11 @@ const StoreSelect: FunctionComponent<{}> = () => { {selectedStore == Store.Custom && ( setSelectedStoreURL(e?.target.value || null)} /> diff --git a/frontend/src/components/settings/pages/general/Updater.tsx b/frontend/src/components/settings/pages/general/Updater.tsx index 1ee31e6c..927a99b0 100644 --- a/frontend/src/components/settings/pages/general/Updater.tsx +++ b/frontend/src/components/settings/pages/general/Updater.tsx @@ -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 ( @@ -45,7 +47,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n {versionInfo.all[id].body}
) : ( - 'no patch notes for this version' + t('Updater.no_patch_notes_desc') )} @@ -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} /> @@ -72,6 +74,8 @@ export default function UpdaterSettings() { const [updateProgress, setUpdateProgress] = useState(-1); const [reloading, setReloading] = useState(false); + const { t } = useTranslation(); + useEffect(() => { window.DeckyUpdater = { updateProgress: (i) => { @@ -93,14 +97,14 @@ export default function UpdaterSettings() { return ( <> Up to date: running {versionInfo?.current} + {t('Updater.updates.lat_version', { ver: versionInfo?.current })} ) } 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')} ) : ( )} diff --git a/frontend/src/components/settings/pages/general/index.tsx b/frontend/src/components/settings/pages/general/index.tsx index 97fd3e42..96ae6782 100644 --- a/frontend/src/components/settings/pages/general/index.tsx +++ b/frontend/src/components/settings/pages/general/index.tsx @@ -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 ( - Updates + {t('SettingsGeneralIndex.updates.header')} - Beta Participation + {t('SettingsGeneralIndex.beta.header')} - Other - + {t('SettingsGeneralIndex.other.header')} + { @@ -37,8 +39,8 @@ export default function GeneralSettings({ - About - + {t('SettingsGeneralIndex.about.header')} +
{versionInfo?.current}
diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx index ac954601..d7ff7bd9 100644 --- a/frontend/src/components/settings/pages/plugin_list/index.tsx +++ b/frontend/src/components/settings/pages/plugin_list/index.tsx @@ -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 }) { const data = props.entry.data; + const { t } = useTranslation(); let pluginName = labelToName(props.entry.label, data?.version); const showCtxMenu = (e: MouseEvent | GamepadEvent) => { showContextMenu( - - window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}>Reload - window.DeckyPluginLoader.uninstallPlugin(pluginName)}>Uninstall + + window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}> + {t('PluginListIndex.reload')} + + + 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')} + , e.currentTarget ?? window, ); @@ -48,11 +64,11 @@ function PluginInteractables(props: { entry: ReorderableEntry }) { {data?.update ? ( 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)} >
- Update to {data?.update?.name} + {t('PluginListIndex.update_to', { name: data?.update?.name })}
@@ -63,7 +79,7 @@ function PluginInteractables(props: { entry: ReorderableEntry }) { onOKButton={() => reinstallPlugin(pluginName, data?.version)} >
- Reinstall + {t('PluginListIndex.reinstall')}
@@ -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 (
-

No plugins installed

+

{t('PluginListIndex.no_plugin')}

); } diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index 828d3ae9..b8c622db 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -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 = ({ plugin }) => { const [selectedOption, setSelectedOption] = useState(0); const root: boolean = plugin.tags.some((tag) => tag === 'root'); + const { t } = useTranslation(); + return (
= ({ plugin }) => { plugin.description ) : ( - No description provided. + {t('PluginCard.plugin_no_desc')} )} @@ -109,7 +113,7 @@ const PluginCard: FC = ({ plugin }) => { color: '#fee75c', }} > - This plugin has full access to your Steam Deck.{' '} + {t('PluginCard.plugin_full_access')}{' '} = ({ plugin }) => { requestPluginInstall(plugin.name, plugin.versions[selectedOption])} + onClick={() => + requestPluginInstall(plugin.name, plugin.versions[selectedOption], InstallType.INSTALL) + } > - Install + {t('PluginCard.plugin_install')}
= ({ plugin }) => { label: version.name, })) as SingleDropdownOption[] } - menuLabel="Plugin Version" + menuLabel={t('PluginCard.plugin_version_label') as string} selectedOption={selectedOption} onChange={({ data }) => setSelectedOption(data)} /> diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index 68f6c077..f2d941cd 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -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: , id: 'browse', renderTabAddon: () => {data.length}, }, { - title: 'About', + title: t('Store.store_tabs.about'), content: , 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%', }} > - Sort + {t("Store.store_sort.label")} setSort(e.data)} /> @@ -122,11 +127,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => { marginLeft: 'auto', }} > - Filter + {t("Store.store_filter.label")} setFilter(e.data)} /> @@ -136,7 +141,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
- setSearchValue(e.target.value)} /> + setSearchValue(e.target.value)} />
@@ -151,11 +156,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => { maxWidth: '100%', }} > - Sort + {t('Store.store_sort.label')} setSort(e.data)} /> @@ -165,7 +170,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
- setSearchValue(e.target.value)} /> + setSearchValue(e.target.value)} + />
@@ -192,6 +201,8 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => { }; const AboutTab: FC<{}> = () => { + const { t } = useTranslation(); + return (
= () => { /> Testing - Please consider testing new plugins to help the Decky Loader team!{' '} + {t('Store.store_testing_cta')}{' '} = () => { deckbrew.xyz/testing - Contributing - - 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. - - Source Code - All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub. + {t('Store.store_contrib.label')} + {t('Store.store_contrib.desc')} + {t('Store.store_source.label')} + {t('Store.store_source.desc')}
); }; diff --git a/frontend/src/developer.tsx b/frontend/src/developer.tsx index 1d6b3fb2..56d28fbf 100644 --- a/frontend/src/developer.tsx +++ b/frontend/src/developer.tsx @@ -18,6 +18,7 @@ import { staticClasses, updaterFieldClasses, } from 'decky-frontend-lib'; +import { useTranslation } from 'react-i18next'; import { FaReact } from 'react-icons/fa'; import Logger from './logger'; @@ -58,9 +59,11 @@ export async function setShowValveInternal(show: boolean) { } export async function setShouldConnectToReactDevTools(enable: boolean) { + const { t } = useTranslation(); + window.DeckyPluginLoader.toaster.toast({ - title: (enable ? 'Enabling' : 'Disabling') + ' React DevTools', - body: 'Reloading in 5 seconds', + title: (enable ? t('Developer.enabling') : t('Developer.disabling')) + ' React DevTools', + body: t('Developer.5secreload'), icon: , }); await sleep(5000); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index eafa9616..27217f96 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,4 +1,8 @@ import { Navigation, Router, sleep } from 'decky-frontend-lib'; +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from 'i18next-http-backend'; +import { initReactI18next } from 'react-i18next'; import PluginLoader from './plugin-loader'; import { DeckyUpdater } from './updater'; @@ -36,9 +40,35 @@ declare global { (async () => { window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text()); + i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + load: 'currentOnly', + detection: { + order: ['querystring', 'navigator'], + lookupQuerystring: 'lng', + }, + //debug: true, + fallbackLng: 'en-US', + interpolation: { + escapeValue: true, + }, + returnEmptyString: false, + backend: { + loadPath: 'http://127.0.0.1:1337/locales/{{lng}}.json', + customHeaders: { + Authentication: window.deckyAuthToken, + }, + requestOptions: { + credentials: 'include', + }, + }, + }); + window.DeckyPluginLoader?.dismountAll(); window.DeckyPluginLoader?.deinit(); - window.DeckyPluginLoader = new PluginLoader(); window.DeckyPluginLoader.init(); window.importDeckyPlugin = function (name: string, version: string) { @@ -62,3 +92,5 @@ declare global { setTimeout(() => window.syncDeckyPlugins(), 5000); })(); + +export default i18n; diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index a3381de7..7bc02c9a 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -98,7 +98,9 @@ class PluginLoader extends Logger { const versionInfo = await this.updateVersion(); if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) { this.toaster.toast({ + //title: t('PluginLoader.decky_title'), title: 'Decky', + //body: t('PluginLoader.decky_update_available', { tag_name: versionInfo?.remote?.tag_name }), body: `Update to ${versionInfo?.remote?.tag_name} available!`, onClick: () => Router.Navigate('/decky/settings'), }); @@ -118,26 +120,35 @@ class PluginLoader extends Logger { const updates = await this.checkPluginUpdates(); if (updates?.size > 0) { this.toaster.toast({ + //title: t('PluginLoader.decky_title'), title: 'Decky', + //body: t('PluginLoader.plugin_update', { count: updates.size }), body: `Updates available for ${updates.size} plugin${updates.size > 1 ? 's' : ''}!`, onClick: () => Router.Navigate('/decky/settings/plugins'), }); } } - public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) { + public addPluginInstallPrompt( + artifact: string, + version: string, + request_id: string, + hash: string, + install_type: number, + ) { showModal( this.callServerMethod('confirm_plugin_install', { request_id })} onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })} />, ); } - public uninstallPlugin(name: string) { + public uninstallPlugin(name: string, title: string, button_text: string, description: string) { showModal( { @@ -146,10 +157,10 @@ class PluginLoader extends Logger { onCancel={() => { // do nothing }} - strTitle={`Uninstall ${name}`} - strOKButtonText={'Uninstall'} + strTitle={title} + strOKButtonText={button_text} > - Are you sure you want to uninstall {name}? + {description} , ); } @@ -242,7 +253,17 @@ class PluginLoader extends Logger { version: version, }); } catch (e) { + //this.error(t('PluginLoader.plugin_load_error.message', { name: name }), e); this.error('Error loading plugin ' + name, e); + /*const TheError: FC<{}> = () => ( + <> + {t('PluginLoader.error')}:{' '} +
+              {e instanceof Error ? e.stack : JSON.stringify(e)}
+            
+ <>{t('PluginLoader.plugin_error_uninstall', { icon: "" })} + + );*/ const TheError: FC<{}> = () => ( <> Error:{' '} @@ -261,7 +282,12 @@ class PluginLoader extends Logger { content: , icon: , }); - this.toaster.toast({ title: 'Error loading ' + name, body: '' + e, icon: }); + this.toaster.toast({ + //title: t('PluginLoader.plugin_load_error.toast', { name: name }), + title: 'Error loading ' + name, + body: '' + e, + icon: , + }); } } else throw new Error(`${name} frontend_bundle not OK`); } diff --git a/frontend/src/plugin.ts b/frontend/src/plugin.ts index 37348593..c8467580 100644 --- a/frontend/src/plugin.ts +++ b/frontend/src/plugin.ts @@ -6,3 +6,9 @@ export interface Plugin { onDismount?(): void; alwaysRender?: boolean; } + +export enum InstallType { + INSTALL, + REINSTALL, + UPDATE, +} diff --git a/frontend/src/store.tsx b/frontend/src/store.tsx index 7ed71e2a..80485252 100644 --- a/frontend/src/store.tsx +++ b/frontend/src/store.tsx @@ -1,4 +1,4 @@ -import { Plugin } from './plugin'; +import { InstallType, Plugin } from './plugin'; import { getSetting, setSetting } from './utils/settings'; export enum Store { @@ -73,7 +73,7 @@ export async function installFromURL(url: string) { }); } -export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) { +export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion, installType: InstallType) { const artifactUrl = selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`; await window.DeckyPluginLoader.callServerMethod('install_plugin', { @@ -81,6 +81,7 @@ export async function requestPluginInstall(plugin: string, selectedVer: StorePlu artifact: artifactUrl, version: selectedVer.name, hash: selectedVer.hash, + install_type: installType, }); } -- cgit v1.2.3