plugins panel keyed (414 -> 383)
AvailableDetail was an implicit-return arrow, so it got a body to hold the hook.
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
type PluginItem,
|
type PluginItem,
|
||||||
type PluginVerb,
|
type PluginVerb,
|
||||||
} from './usePlugins';
|
} from './usePlugins';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
// The right panel: one plugin, and its verbs — install, enable, disable, update, delete.
|
// The right panel: one plugin, and its verbs — install, enable, disable, update, delete.
|
||||||
//
|
//
|
||||||
@@ -80,11 +81,12 @@ const Parts = ({ has }: { has: PluginItem['has'] }) => {
|
|||||||
* "Requires: nothing" row is noise on every other plugin's page.
|
* "Requires: nothing" row is noise on every other plugin's page.
|
||||||
*/
|
*/
|
||||||
const Dependencies = ({ deps }: { deps: PluginDependencies }) => {
|
const Dependencies = ({ deps }: { deps: PluginDependencies }) => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
if (!deps.items.length) return null;
|
if (!deps.items.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 border-t border-duck-dark/10 pt-4">
|
<div className="mt-4 border-t border-duck-dark/10 pt-4">
|
||||||
<p className="text-xs font-medium uppercase tracking-wide text-duck-dark/40">Needs on this machine</p>
|
<p className="text-xs font-medium uppercase tracking-wide text-duck-dark/40">{t('plugins.needsOnThisMachine')}</p>
|
||||||
<ul className="mt-2 space-y-2">
|
<ul className="mt-2 space-y-2">
|
||||||
{deps.items.map((item) => (
|
{deps.items.map((item) => (
|
||||||
<li key={item.binary} className="flex gap-2 text-sm">
|
<li key={item.binary} className="flex gap-2 text-sm">
|
||||||
@@ -201,56 +203,59 @@ const AvailableDetail = ({
|
|||||||
steps: ActionEntry[];
|
steps: ActionEntry[];
|
||||||
result: PluginActionResult | null;
|
result: PluginActionResult | null;
|
||||||
running: PluginVerb | null;
|
running: PluginVerb | null;
|
||||||
}) => (
|
}) => {
|
||||||
<div className="h-full overflow-auto p-6">
|
const { t } = useTranslation();
|
||||||
<div className="flex items-center gap-3">
|
return (
|
||||||
<AvailableGlyph plugin={plugin} />
|
<div className="h-full overflow-auto p-6">
|
||||||
<div className="min-w-0">
|
<div className="flex items-center gap-3">
|
||||||
<h2 className="text-lg font-semibold text-duck-dark">{plugin.label}</h2>
|
<AvailableGlyph plugin={plugin} />
|
||||||
<p className="mt-0.5 text-sm text-duck-dark/60">{plugin.summary}</p>
|
<div className="min-w-0">
|
||||||
|
<h2 className="text-lg font-semibold text-duck-dark">{plugin.label}</h2>
|
||||||
|
<p className="mt-0.5 text-sm text-duck-dark/60">{plugin.summary}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 border-t border-duck-dark/10 pt-4">
|
<div className="mt-5 border-t border-duck-dark/10 pt-4">
|
||||||
<Row label="Publisher">{plugin.publisher}</Row>
|
<Row label={t('plugins.publisher')}>{plugin.publisher}</Row>
|
||||||
<Row label="Version">{plugin.version}</Row>
|
<Row label={t('plugins.version')}>{plugin.version}</Row>
|
||||||
<Row label="Needs platform">{plugin.platform}</Row>
|
<Row label={t('plugins.needsPlatform')}>{plugin.platform}</Row>
|
||||||
<Row label="Would mount">
|
<Row label={t('plugins.wouldMount')}>
|
||||||
<code>/api{plugin.prefix}</code>
|
<code>/api{plugin.prefix}</code>
|
||||||
</Row>
|
</Row>
|
||||||
<Row label="Ships">
|
<Row label={t('plugins.ships')}>
|
||||||
<Parts has={plugin.has} />
|
<Parts has={plugin.has} />
|
||||||
</Row>
|
</Row>
|
||||||
<Row label="Permissions">
|
<Row label={t('plugins.permissions')}>
|
||||||
{plugin.permissions.length ? plugin.permissions.map((p) => p.key).join(', ') : 'none'}
|
{plugin.permissions.length ? plugin.permissions.map((p) => p.key).join(', ') : 'none'}
|
||||||
</Row>
|
</Row>
|
||||||
{/* Shown because installing this runs `git clone` against it. An owner agreeing to put code on
|
{/* Shown because installing this runs `git clone` against it. An owner agreeing to put code on
|
||||||
their machine should be able to read where it comes from without opening a config file. */}
|
their machine should be able to read where it comes from without opening a config file. */}
|
||||||
<Row label="Source">
|
<Row label={t('plugins.source')}>
|
||||||
<code className="break-all text-xs">{plugin.source}</code>
|
<code className="break-all text-xs">{plugin.source}</code>
|
||||||
</Row>
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dependencies deps={plugin.dependencies} />
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button
|
||||||
|
tone="primary"
|
||||||
|
disabled={busy || (!plugin.dependencies.satisfied && !plugin.dependencies.canInstall)}
|
||||||
|
onClick={onInstall}
|
||||||
|
>
|
||||||
|
{t('plugins.install')}
|
||||||
|
</Button>
|
||||||
|
<p className="mt-2 text-xs text-duck-dark/50">
|
||||||
|
{t('plugins.clonesInto')} <code>plugins/{plugin.appName}</code>, then installs it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionLog steps={steps} result={result} running={running} />
|
||||||
|
|
||||||
|
<Readme appName={plugin.appName} />
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<Dependencies deps={plugin.dependencies} />
|
};
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<Button
|
|
||||||
tone="primary"
|
|
||||||
disabled={busy || (!plugin.dependencies.satisfied && !plugin.dependencies.canInstall)}
|
|
||||||
onClick={onInstall}
|
|
||||||
>
|
|
||||||
Install
|
|
||||||
</Button>
|
|
||||||
<p className="mt-2 text-xs text-duck-dark/50">
|
|
||||||
Clones the repository into <code>plugins/{plugin.appName}</code>, then installs it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ActionLog steps={steps} result={result} running={running} />
|
|
||||||
|
|
||||||
<Readme appName={plugin.appName} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the marketplace says about a plugin, in the pane where somebody decides whether to install it.
|
* What the marketplace says about a plugin, in the pane where somebody decides whether to install it.
|
||||||
@@ -317,6 +322,7 @@ const DeletePlugin = ({
|
|||||||
busy: boolean;
|
busy: boolean;
|
||||||
onDelete: (dropData: boolean) => void;
|
onDelete: (dropData: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [dropData, setDropData] = useState(false);
|
const [dropData, setDropData] = useState(false);
|
||||||
|
|
||||||
@@ -334,7 +340,7 @@ const DeletePlugin = ({
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
{t('common.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -360,7 +366,7 @@ const DeletePlugin = ({
|
|||||||
className="mt-0.5"
|
className="mt-0.5"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-duck-dark">
|
<span className="text-xs text-duck-dark">
|
||||||
<span className="font-medium">Also delete all its data</span>
|
<span className="font-medium">{t('plugins.deleteData')}</span>
|
||||||
<span className="mt-0.5 block text-duck-dark/60">
|
<span className="mt-0.5 block text-duck-dark/60">
|
||||||
Drops every table this plugin owns. Without this its rows stay, and reinstalling later finds them
|
Drops every table this plugin owns. Without this its rows stay, and reinstalling later finds them
|
||||||
where they were.
|
where they were.
|
||||||
@@ -368,12 +374,12 @@ const DeletePlugin = ({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-duck-dark/50">This plugin declares no tables, so there is no data to keep.</p>
|
<p className="text-xs text-duck-dark/50">{t('plugins.noTables')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter className="mt-2">
|
<DialogFooter className="mt-2">
|
||||||
<Button disabled={busy} onClick={() => setOpen(false)}>
|
<Button disabled={busy} onClick={() => setOpen(false)}>
|
||||||
Cancel
|
{tUi('button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
tone="danger"
|
tone="danger"
|
||||||
@@ -383,7 +389,7 @@ const DeletePlugin = ({
|
|||||||
onDelete(dropData);
|
onDelete(dropData);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{dropData ? 'Delete it and its data' : `Delete ${plugin.label}`}
|
{dropData ? 'Delete it and its data' : t('plugins.deleteNamed', { name: plugin.label })}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -393,6 +399,7 @@ const DeletePlugin = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PluginDetail = () => {
|
export const PluginDetail = () => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
const { plugins, available, steps, result, running, run } = usePlugins();
|
const { plugins, available, steps, result, running, run } = usePlugins();
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
const selected = params.get('selected');
|
const selected = params.get('selected');
|
||||||
@@ -417,7 +424,7 @@ export const PluginDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
return <div className="p-6 text-sm text-duck-dark/50">Select a plugin.</div>;
|
return <div className="p-6 text-sm text-duck-dark/50">{t('plugins.selectOne')}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -431,22 +438,22 @@ export const PluginDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 border-t border-duck-dark/10 pt-4">
|
<div className="mt-5 border-t border-duck-dark/10 pt-4">
|
||||||
<Row label="Mounts at">
|
<Row label={t('plugins.mountsAt')}>
|
||||||
<code>/api{plugin.prefix}</code>
|
<code>/api{plugin.prefix}</code>
|
||||||
</Row>
|
</Row>
|
||||||
<Row label="Publisher">{plugin.publisher}</Row>
|
<Row label={t('plugins.publisher')}>{plugin.publisher}</Row>
|
||||||
<Row label="Version">
|
<Row label={t('plugins.version')}>
|
||||||
{plugin.version}
|
{plugin.version}
|
||||||
{plugin.outdated ? (
|
{plugin.outdated ? (
|
||||||
<span className="ml-2 text-busy-ink">on disk — installed {plugin.installedVersion}</span>
|
<span className="ml-2 text-busy-ink">on disk — installed {plugin.installedVersion}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</Row>
|
</Row>
|
||||||
<Row label="Needs platform">{plugin.platform}</Row>
|
<Row label={t('plugins.needsPlatform')}>{plugin.platform}</Row>
|
||||||
<Row label="Ships">
|
<Row label={t('plugins.ships')}>
|
||||||
<Parts has={plugin.has} />
|
<Parts has={plugin.has} />
|
||||||
</Row>
|
</Row>
|
||||||
{plugin.has.sidecar ? (
|
{plugin.has.sidecar ? (
|
||||||
<Row label="Sidecar">
|
<Row label={t('plugins.sidecar')}>
|
||||||
{/* Enabled but not online is the state worth naming: the plugin is switched on and its
|
{/* Enabled but not online is the state worth naming: the plugin is switched on and its
|
||||||
process is not running, which is broken rather than off. */}
|
process is not running, which is broken rather than off. */}
|
||||||
<span
|
<span
|
||||||
@@ -460,7 +467,7 @@ export const PluginDetail = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
<Row label="Permissions">
|
<Row label={t('plugins.permissions')}>
|
||||||
{plugin.permissions.length
|
{plugin.permissions.length
|
||||||
? plugin.permissions.map((p) => `${p.key}${p.ownerOnly ? ' (owner only)' : ''}`).join(', ')
|
? plugin.permissions.map((p) => `${p.key}${p.ownerOnly ? ' (owner only)' : ''}`).join(', ')
|
||||||
: 'none — reachable by anyone who can reach the platform'}
|
: 'none — reachable by anyone who can reach the platform'}
|
||||||
@@ -494,7 +501,7 @@ export const PluginDetail = () => {
|
|||||||
disabled={busy || (!plugin.dependencies.satisfied && !plugin.dependencies.canInstall)}
|
disabled={busy || (!plugin.dependencies.satisfied && !plugin.dependencies.canInstall)}
|
||||||
onClick={() => run('install', plugin.appName)}
|
onClick={() => run('install', plugin.appName)}
|
||||||
>
|
>
|
||||||
Install
|
{t('plugins.install')}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{!plugin.installed ? (
|
{!plugin.installed ? (
|
||||||
@@ -508,11 +515,11 @@ export const PluginDetail = () => {
|
|||||||
<>
|
<>
|
||||||
{plugin.enabled ? (
|
{plugin.enabled ? (
|
||||||
<Button disabled={busy} onClick={() => run('disable', plugin.appName)}>
|
<Button disabled={busy} onClick={() => run('disable', plugin.appName)}>
|
||||||
Disable
|
{t('plugins.disable')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button tone="primary" disabled={busy} onClick={() => run('enable', plugin.appName)}>
|
<Button tone="primary" disabled={busy} onClick={() => run('enable', plugin.appName)}>
|
||||||
Enable
|
{t('plugins.enable')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Always offered, not gated on `outdated`. That flag compares the manifest's version to the
|
{/* Always offered, not gated on `outdated`. That flag compares the manifest's version to the
|
||||||
@@ -521,7 +528,7 @@ export const PluginDetail = () => {
|
|||||||
which cannot fetch: the directory exists, so the clone is skipped and only the apply
|
which cannot fetch: the directory exists, so the clone is skipped and only the apply
|
||||||
steps re-ran. `update` pulls first. */}
|
steps re-ran. `update` pulls first. */}
|
||||||
<Button disabled={busy} onClick={() => run('update', plugin.appName)}>
|
<Button disabled={busy} onClick={() => run('update', plugin.appName)}>
|
||||||
{plugin.outdated ? `Update to ${plugin.version}` : 'Update'}
|
{plugin.outdated ? t('plugins.updateTo', { version: plugin.version }) : 'Update'}
|
||||||
</Button>
|
</Button>
|
||||||
{/* Absent while enabled — disable first. `DeletePlugin` decides that itself rather than being
|
{/* Absent while enabled — disable first. `DeletePlugin` decides that itself rather than being
|
||||||
conditioned here, so the rule lives next to the confirmation that explains it. */}
|
conditioned here, so the rule lives next to the confirmation that explains it. */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Link, useSearchParams } from 'react-router';
|
|||||||
import { Puzzle, AlertTriangle, Database } from 'lucide-react';
|
import { Puzzle, AlertTriangle, Database } from 'lucide-react';
|
||||||
import { usePlugins, type OrphanTable, type PluginItem } from './usePlugins';
|
import { usePlugins, type OrphanTable, type PluginItem } from './usePlugins';
|
||||||
import { resolveIcon } from '../../utils/resolve-icon';
|
import { resolveIcon } from '../../utils/resolve-icon';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
// The left panel: every plugin in the tree, installed or not.
|
// The left panel: every plugin in the tree, installed or not.
|
||||||
//
|
//
|
||||||
@@ -17,11 +18,12 @@ const Status = ({ plugin }: { plugin: PluginItem }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PluginsList = () => {
|
export const PluginsList = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { plugins, available, broken, orphans, isLoading, updateAll, updatingAll, updateAllResult } = usePlugins();
|
const { plugins, available, broken, orphans, isLoading, updateAll, updatingAll, updateAllResult } = usePlugins();
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
const selected = params.get('selected');
|
const selected = params.get('selected');
|
||||||
|
|
||||||
if (isLoading) return <div className="p-4 text-sm text-duck-dark/50">Loading…</div>;
|
if (isLoading) return <div className="p-4 text-sm text-duck-dark/50">{t('plugins.loading')}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
@@ -34,11 +36,13 @@ export const PluginsList = () => {
|
|||||||
that: a count would imply a check that has not happened. */}
|
that: a count would imply a check that has not happened. */}
|
||||||
{plugins.length ? (
|
{plugins.length ? (
|
||||||
<div className="flex items-center justify-between gap-2 border-b border-duck-dark/5 px-3 py-2">
|
<div className="flex items-center justify-between gap-2 border-b border-duck-dark/5 px-3 py-2">
|
||||||
{/* "On this machine", not "Installed" — the list is every plugin DIRECTORY, and an uninstalled
|
{/* t('plugins.onThisMachine'), not "Installed" — the list is every plugin DIRECTORY, and an uninstalled
|
||||||
one stays in it. Under the old heading such a row read "Installed · not installed", which is
|
one stays in it. Under the old heading such a row read "Installed · not installed", which is
|
||||||
how a real uninstall got mistaken for a failed one: the plugin never moved, so nothing on
|
how a real uninstall got mistaken for a failed one: the plugin never moved, so nothing on
|
||||||
screen said the directory was still there deciding what a reinstall would run. */}
|
screen said the directory was still there deciding what a reinstall would run. */}
|
||||||
<span className="text-xs font-medium uppercase tracking-wide text-duck-dark/40">On this machine</span>
|
<span className="text-xs font-medium uppercase tracking-wide text-duck-dark/40">
|
||||||
|
{t('plugins.onThisMachine')}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void updateAll()}
|
onClick={() => void updateAll()}
|
||||||
@@ -73,7 +77,7 @@ export const PluginsList = () => {
|
|||||||
|
|
||||||
{plugins.length === 0 && available.length === 0 && broken.length === 0 ? (
|
{plugins.length === 0 && available.length === 0 && broken.length === 0 ? (
|
||||||
<div className="p-4 text-sm text-duck-dark/50">
|
<div className="p-4 text-sm text-duck-dark/50">
|
||||||
No plugins in <code>plugins/</code> yet.
|
{t('plugins.noPluginsIn')} <code>plugins/</code> yet.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -113,7 +117,9 @@ export const PluginsList = () => {
|
|||||||
directory and the next read discovers it like any other. */}
|
directory and the next read discovers it like any other. */}
|
||||||
{available.length ? (
|
{available.length ? (
|
||||||
<div className="border-b border-duck-dark/5 bg-duck-dark/[0.02] px-3 py-1.5">
|
<div className="border-b border-duck-dark/5 bg-duck-dark/[0.02] px-3 py-1.5">
|
||||||
<span className="text-xs font-medium uppercase tracking-wide text-duck-dark/40">Available</span>
|
<span className="text-xs font-medium uppercase tracking-wide text-duck-dark/40">
|
||||||
|
{t('plugins.available')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{available.map((plugin) => (
|
{available.map((plugin) => (
|
||||||
|
|||||||
@@ -188,5 +188,30 @@
|
|||||||
"extractedTo": "Extracted to “{{name}}”",
|
"extractedTo": "Extracted to “{{name}}”",
|
||||||
"downloadInstead": "Download instead",
|
"downloadInstead": "Download instead",
|
||||||
"archiveFile": "Archive file"
|
"archiveFile": "Archive file"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"loading": "Loading…",
|
||||||
|
"onThisMachine": "On this machine",
|
||||||
|
"noPluginsIn": "No plugins in",
|
||||||
|
"available": "Available",
|
||||||
|
"needsOnThisMachine": "Needs on this machine",
|
||||||
|
"install": "Install",
|
||||||
|
"clonesInto": "Clones the repository into",
|
||||||
|
"deleteData": "Also delete all its data",
|
||||||
|
"noTables": "This plugin declares no tables, so there is no data to keep.",
|
||||||
|
"selectOne": "Select a plugin.",
|
||||||
|
"disable": "Disable",
|
||||||
|
"enable": "Enable",
|
||||||
|
"publisher": "Publisher",
|
||||||
|
"version": "Version",
|
||||||
|
"needsPlatform": "Needs platform",
|
||||||
|
"wouldMount": "Would mount",
|
||||||
|
"ships": "Ships",
|
||||||
|
"permissions": "Permissions",
|
||||||
|
"source": "Source",
|
||||||
|
"mountsAt": "Mounts at",
|
||||||
|
"sidecar": "Sidecar",
|
||||||
|
"deleteNamed": "Delete {{name}}",
|
||||||
|
"updateTo": "Update to {{version}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,5 +188,30 @@
|
|||||||
"extractedTo": "Extraído para “{{name}}”",
|
"extractedTo": "Extraído para “{{name}}”",
|
||||||
"downloadInstead": "Descarregar em vez disso",
|
"downloadInstead": "Descarregar em vez disso",
|
||||||
"archiveFile": "Ficheiro de arquivo"
|
"archiveFile": "Ficheiro de arquivo"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"loading": "A carregar…",
|
||||||
|
"onThisMachine": "Nesta máquina",
|
||||||
|
"noPluginsIn": "Sem plugins em",
|
||||||
|
"available": "Disponíveis",
|
||||||
|
"needsOnThisMachine": "Requisitos nesta máquina",
|
||||||
|
"install": "Instalar",
|
||||||
|
"clonesInto": "Clona o repositório para",
|
||||||
|
"deleteData": "Eliminar também todos os seus dados",
|
||||||
|
"noTables": "Este plugin não declara tabelas, pelo que não há dados a guardar.",
|
||||||
|
"selectOne": "Selecione um plugin.",
|
||||||
|
"disable": "Desativar",
|
||||||
|
"enable": "Ativar",
|
||||||
|
"publisher": "Editor",
|
||||||
|
"version": "Versão",
|
||||||
|
"needsPlatform": "Requer a plataforma",
|
||||||
|
"wouldMount": "Montaria em",
|
||||||
|
"ships": "Inclui",
|
||||||
|
"permissions": "Permissões",
|
||||||
|
"source": "Origem",
|
||||||
|
"mountsAt": "Monta em",
|
||||||
|
"sidecar": "Sidecar",
|
||||||
|
"deleteNamed": "Eliminar {{name}}",
|
||||||
|
"updateTo": "Atualizar para {{version}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user