feat: sync with local plugin status in store (#733)

* fix: useDeckyState proper type and safety

* refactor: plugin list

Avoids unneeded re-renders. See https://react.dev/learn/you-might-not-need-an-effect#caching-expensive-calculations

* feat: sync with local plugin status in store

Adds some QoL changes to the plugin store browser:

- Add ✓ icon to currently installed plugin version in version selector
- Change install button label depending on the install type that the
  button would trigger
- Adds icon to install button for clarity

The goal is to make it clear to the user what the current state of the
installed plugin is, and what would be the impact of installing the
selected version.

Resolves #360

* lint: prettier

* fix: add missing translations

* refactor: safer translation strings on install

Prefer using `t(...)` instead of `TranslationHelper` since it ensures
that the translation keys are not missing in the locale files when
running the `extractext` task.

By adding comments with `t(...)` calls, `i18next-parser` will generate
the strings as if they were present as literals in the code (see
https://github.com/i18next/i18next-parser#caveats).

This does _not_ suppress the warnings (since `i18next-parser` does not
have access to TS types, so it cannot infer template literals) but it at
least makes it less likely that a translation will be missed by mistake,
have typos, etc.
This commit is contained in:
Álvaro Cuesta
2025-01-02 20:38:40 +01:00
committed by GitHub
parent 79bb62a3c4
commit f6144f9634
11 changed files with 211 additions and 99 deletions
@@ -3,7 +3,7 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaDownload } from 'react-icons/fa';
import { InstallType } from '../../plugin';
import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
@@ -12,13 +12,7 @@ interface MultiplePluginsInstallModalProps {
closeModal?(): void;
}
// values are the JSON keys used in the translation file
const InstallTypeTranslationMapping = {
[InstallType.INSTALL]: 'install',
[InstallType.REINSTALL]: 'reinstall',
[InstallType.UPDATE]: 'update',
} as const satisfies Record<InstallType, string>;
// IMPORTANT! Keep in sync with `t(...)` comments below
type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType];
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
@@ -70,6 +64,8 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install';
if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall';
if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update';
if (requests.every(({ install_type }) => install_type === InstallType.DOWNGRADE)) return 'downgrade';
if (requests.every(({ install_type }) => install_type === InstallType.OVERWRITE)) return 'overwrite';
return 'mixed';
}, [requests]);
@@ -86,14 +82,35 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
onCancel={async () => {
await onCancel();
}}
strTitle={<div>{t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })}</div>}
strOKButtonText={t(`MultiplePluginsInstallModal.ok_button.${loading ? 'loading' : 'idle'}`)}
strTitle={
<div>
{
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('MultiplePluginsInstallModal.title.install', { count: n })
// t('MultiplePluginsInstallModal.title.reinstall', { count: n })
// t('MultiplePluginsInstallModal.title.update', { count: n })
// t('MultiplePluginsInstallModal.title.downgrade', { count: n })
// t('MultiplePluginsInstallModal.title.overwrite', { count: n })
// t('MultiplePluginsInstallModal.title.mixed', { count: n })
t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })
}
</div>
}
strOKButtonText={
loading ? t('MultiplePluginsInstallModal.ok_button.loading') : t('MultiplePluginsInstallModal.ok_button.idle')
}
>
<div>
{t('MultiplePluginsInstallModal.confirm')}
<ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{requests.map(({ name, version, install_type, hash }, i) => {
const installTypeStr = InstallTypeTranslationMapping[install_type];
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('MultiplePluginsInstallModal.description.install')
// t('MultiplePluginsInstallModal.description.reinstall')
// t('MultiplePluginsInstallModal.description.update')
// t('MultiplePluginsInstallModal.description.downgrade')
// t('MultiplePluginsInstallModal.description.overwrite')
const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, {
name,
version,
@@ -2,13 +2,13 @@ import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
interface PluginInstallModalProps {
artifact: string;
version: string;
hash: string;
installType: number;
installType: InstallType;
onOK(): void;
onCancel(): void;
closeModal?(): void;
@@ -44,6 +44,8 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
};
}, []);
const installTypeTranslationKey = InstallTypeTranslationMapping[installType];
return (
<ConfirmModal
bOKDisabled={loading}
@@ -59,12 +61,15 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
}}
strTitle={
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%' }}>
<TranslationHelper
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="title"
i18nArgs={{ artifact: artifact }}
installType={installType}
/>
{
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('PluginInstallModal.install.title')
// t('PluginInstallModal.reinstall.title')
// t('PluginInstallModal.update.title')
// t('PluginInstallModal.downgrade.title')
// t('PluginInstallModal.overwrite.title')
t(`PluginInstallModal.${installTypeTranslationKey}.title`, { artifact: artifact })
}
{loading && (
<div style={{ marginLeft: 'auto' }}>
<ProgressBarWithInfo
@@ -80,33 +85,44 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
strOKButtonText={
loading ? (
<div>
<TranslationHelper
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="button_processing"
installType={installType}
/>
{
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('PluginInstallModal.install.button_processing')
// t('PluginInstallModal.reinstall.button_processing')
// t('PluginInstallModal.update.button_processing')
// t('PluginInstallModal.downgrade.button_processing')
// t('PluginInstallModal.overwrite.button_processing')
t(`PluginInstallModal.${installTypeTranslationKey}.button_processing`)
}
</div>
) : (
<div>
<TranslationHelper
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="button_idle"
installType={installType}
/>
{
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('PluginInstallModal.install.button_idle')
// t('PluginInstallModal.reinstall.button_idle')
// t('PluginInstallModal.update.button_idle')
// t('PluginInstallModal.downgrade.button_idle')
// t('PluginInstallModal.overwrite.button_idle')
t(`PluginInstallModal.${installTypeTranslationKey}.button_idle`)
}
</div>
)
}
>
<div>
<TranslationHelper
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="desc"
i18nArgs={{
{
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('PluginInstallModal.install.desc')
// t('PluginInstallModal.reinstall.desc')
// t('PluginInstallModal.update.desc')
// t('PluginInstallModal.downgrade.desc')
// t('PluginInstallModal.overwrite.desc')
t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
artifact: artifact,
version: version,
}}
installType={installType}
/>
})
}
</div>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal>