feat(admin): addons tab in the cards of the other tabs, with a search and an on/off filter

The addons sit in a card per type now, the way every other admin tab sets
its cards, instead of three columns nested in one card. A head card shows
how many of each type are on and finds an addon by its name or description,
with a switch between all, the ones on and the ones off.
This commit is contained in:
Maurice
2026-10-24 14:49:17 +02:00
parent 26a6147e8d
commit 486a2eeca3
29 changed files with 331 additions and 61 deletions
@@ -712,4 +712,33 @@ describe('AddonManager', () => {
expect(roadtrip.querySelector('svg.lucide-car-front')).toBeInTheDocument();
expect(tours.querySelector('svg.lucide-puzzle')).not.toBeInTheDocument();
});
it('FE-ADMIN-ADDON-045: finds an addon by name or description and shows the ones on or off', async () => {
const user = userEvent.setup();
server.use(
addonsRoute([
buildAddon({ id: 'x-lists', name: 'Todo List', description: 'Track tasks', type: 'trip', enabled: true }),
buildAddon({ id: 'x-costs', name: 'Budget', description: 'Split costs', type: 'trip', enabled: false }),
buildAddon({ id: 'x-holiday', name: 'Vacay', description: 'Holiday planner', type: 'global', enabled: true }),
])
);
render(<AddonManager />);
await screen.findByText('Todo List');
await user.type(screen.getByRole('searchbox', { name: 'Search addons' }), 'costs');
expect(screen.getByText('Budget')).toBeInTheDocument();
expect(screen.queryByText('Todo List')).toBeNull();
expect(screen.queryByText('Vacay')).toBeNull();
await user.clear(screen.getByRole('searchbox', { name: 'Search addons' }));
await user.click(screen.getByRole('button', { name: 'On' }));
expect(screen.getByText('Todo List')).toBeInTheDocument();
expect(screen.getByText('Vacay')).toBeInTheDocument();
expect(screen.queryByText('Budget')).toBeNull();
await user.click(screen.getByRole('button', { name: 'Off' }));
await user.type(screen.getByRole('searchbox', { name: 'Search addons' }), 'nothing like this');
expect(screen.getByText('No addon matches the search or the filter.')).toBeInTheDocument();
});
});
+140 -61
View File
@@ -1,7 +1,7 @@
import type { ComponentType } from 'react'
import { useState, type ComponentType } from 'react'
import { useTranslation } from '../../i18n'
import { useIsDark } from '../../hooks/useIsDark'
import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Sparkles, Luggage, Plane, Bookmark, Users, Loader2, Mountain, CarFront } from 'lucide-react'
import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Sparkles, Luggage, Plane, Bookmark, Users, Loader2, Mountain, CarFront, Search } from 'lucide-react'
import EmptyState from '../shared/EmptyState'
import DawarichIcon from '../shared/DawarichIcon'
import AirTrailIcon from '../shared/AirTrailIcon'
@@ -76,6 +76,8 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
toggleToursWanderer,
mapSwitches,
} = useAddonManager()
const [query, setQuery] = useState('')
const [view, setView] = useState<AddonView>('all')
if (loading) {
return (
@@ -194,10 +196,10 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
)
}
/* One column per type, side by side, instead of three stacked sections. Stacked, each
section opened its own set of columns and the tall tiles (Collab, Journey) set a
section's height while its neighbours ran out early. Side by side the column's
head carries the type, so the tiles need no badge of their own. */
/* A card per type, the way every other admin tab sets its cards: Trip in the
left column, Global and Integration in the right one, each card with the
dialogs' head band carrying the type, its count and what it means. Above
them one card says how many are on and finds an addon by name. */
const groups = [
{ key: 'trip', addons: tripAddons },
{ key: 'global', addons: globalAddons },
@@ -214,68 +216,145 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
</span>
)
return (
<SettingsCard
icon={Puzzle}
title={t('admin.addons.title')}
hint={subtitle}
badge={totalCount > 0 ? (
<Tooltip label={t('admin.addons.group.count', { enabled: enabledCount, total: totalCount })}>
<span className="inline-flex">
<StatusPill tone={enabledCount > 0 ? 'success' : 'neutral'}>{enabledCount}/{totalCount}</StatusPill>
</span>
</Tooltip>
) : undefined}
>
{addons.length === 0 ? (
<EmptyState scene="idle" title={t('admin.addons.noAddons')} surface="var(--bg-secondary)" />
) : (
<div className="grid grid-cols-1 items-start gap-4 md:grid-cols-2 xl:grid-cols-3">
{groups.map(g => {
const meta = TYPE_META[g.key]
return (
<section key={g.key} className="min-w-0 overflow-hidden rounded-[14px] border border-edge-faint bg-surface-card">
<GroupHead icon={meta.icon} label={t(meta.labelKey)} hint={t(meta.hintKey)} addons={g.key === 'integration' ? [...aiAddons, ...g.addons] : g.addons} t={t} />
<div className="divide-y divide-edge-faint">
{/* The AI addons share a connection and a settings dialog, so they head the column as one tile. */}
{g.key === 'integration' && aiAddons.length > 0 && <AiAddonTile addons={aiAddons} onToggle={handleToggle} />}
{g.addons.map(tile)}
</div>
</section>
)
})}
/** Whether an addon is shown under the search and the filter. */
const shown = (addon: Addon) => {
if (view === 'on' && !addon.enabled) return false
if (view === 'off' && addon.enabled) return false
const q = query.trim().toLocaleLowerCase()
if (!q) return true
const label = getAddonLabel(t, addon)
return `${label.name} ${label.description ?? ''}`.toLocaleLowerCase().includes(q)
}
const card = (g: (typeof groups)[number]) => {
const meta = TYPE_META[g.key]
const all = g.key === 'integration' ? [...aiAddons, ...g.addons] : g.addons
const visible = g.addons.filter(shown)
const ai = g.key === 'integration' && aiAddons.some(shown)
if (!visible.length && !ai) return null
const on = all.filter(a => a.enabled).length
return (
<SettingsCard
key={g.key}
icon={meta.icon}
title={t(meta.labelKey)}
hint={t(meta.hintKey)}
badge={(
<Tooltip label={t('admin.addons.group.count', { enabled: on, total: all.length })}>
<span className="inline-flex"><StatusPill tone={on > 0 ? 'success' : 'neutral'}>{on}/{all.length}</StatusPill></span>
</Tooltip>
)}
bodyClassName="p-3"
>
<div className="divide-y divide-edge-faint overflow-hidden rounded-[12px] border border-edge-faint bg-surface-card">
{/* The AI addons share a connection and a settings dialog, so they head the card as one tile. */}
{ai && <AiAddonTile addons={aiAddons} onToggle={handleToggle} />}
{visible.map(tile)}
</div>
)}
</SettingsCard>
</SettingsCard>
)
}
const [left, ...right] = groups
const cards = { left: left ? card(left) : null, right: right.map(card).filter(Boolean) }
return (
<>
<SettingsCard
icon={Puzzle}
title={t('admin.addons.title')}
hint={subtitle}
badge={totalCount > 0 ? (
<Tooltip label={t('admin.addons.group.count', { enabled: enabledCount, total: totalCount })}>
<span className="inline-flex">
<StatusPill tone={enabledCount > 0 ? 'success' : 'neutral'}>{enabledCount}/{totalCount}</StatusPill>
</span>
</Tooltip>
) : undefined}
>
{addons.length === 0
? <EmptyState scene="idle" title={t('admin.addons.noAddons')} surface="var(--bg-secondary)" />
: <AddonToolbar groups={groups.map(g => ({ key: g.key, addons: g.key === 'integration' ? [...aiAddons, ...g.addons] : g.addons }))} query={query} onQuery={setQuery} view={view} onView={setView} t={t} />}
</SettingsCard>
{addons.length > 0 && (cards.left || cards.right.length > 0 ? (
<div className="grid grid-cols-1 gap-x-5 xl:grid-cols-2 xl:items-start">
<div className="min-w-0">{cards.left}</div>
<div className="min-w-0">{cards.right}</div>
</div>
) : (
<p className="m-0 rounded-2xl border border-dashed border-edge-faint px-4 py-8 text-center text-content-muted" style={fs(13, 'body')}>
{t('admin.addons.noMatch')}
</p>
))}
</>
)
}
/** The head band of one type's column: icon tile, the type, its count and what it means. */
function GroupHead({ icon: Icon, label, hint, addons, t }: {
icon: ComponentType<{ size?: number; className?: string; strokeWidth?: number }>
label: string
hint: string
addons: Addon[]
type AddonView = 'all' | 'on' | 'off'
/**
* The head card's body: a tile per type with how many of it are on, then a
* search over the addons' names and descriptions and a switch between all,
* the ones on and the ones off.
*/
function AddonToolbar({ groups, query, onQuery, view, onView, t }: {
groups: { key: string; addons: Addon[] }[]
query: string
onQuery: (q: string) => void
view: AddonView
onView: (v: AddonView) => void
t: (key: string, params?: Record<string, unknown>) => string
}) {
const enabled = addons.filter(a => a.enabled).length
return (
<div className="flex items-start gap-2.5 border-b border-edge-faint bg-surface-tertiary px-3.5 py-2.5">
<span className="mt-px grid h-7 w-7 flex-none place-items-center rounded-[9px] bg-surface-card text-content-secondary shadow-sm">
<Icon size={13} strokeWidth={2} />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h3 className="m-0 min-w-0 flex-1 truncate font-bold text-content" style={fs(13, 'body')}>{label}</h3>
<Tooltip label={t('admin.addons.group.count', { enabled, total: addons.length })}>
<span className="inline-flex">
<StatusPill>{enabled}/{addons.length}</StatusPill>
</span>
</Tooltip>
</div>
{/* Says what the type means: the only place that still explains it. */}
<p className="m-0 mt-0.5 leading-snug text-content-faint" style={fs(11)}>{hint}</p>
<>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
{groups.map(g => {
const meta = TYPE_META[g.key]
const Icon = meta.icon
const on = g.addons.filter(a => a.enabled).length
return (
<div key={g.key} className="flex items-center gap-3 rounded-[12px] border border-edge-faint bg-surface-card px-3 py-2.5">
<span className="grid h-8 w-8 flex-none place-items-center rounded-[10px] bg-surface-secondary text-content-secondary">
<Icon size={15} />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate font-semibold text-content" style={fs(12.5, 'body')}>{t(meta.labelKey)}</span>
<span className="block text-content-faint" style={fs(11)}>{t('admin.addons.group.count', { enabled: on, total: g.addons.length })}</span>
</span>
<span className={`font-geist font-bold tabular-nums ${on > 0 ? 'text-content' : 'text-content-faint'}`} style={fs(18, 'body')} aria-hidden>{on}</span>
</div>
)
})}
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<label className="relative flex min-w-0 flex-1 items-center">
<Search size={14} className="pointer-events-none absolute start-3 text-content-faint" />
<input
type="search"
value={query}
onChange={e => onQuery(e.target.value)}
placeholder={t('admin.addons.search')}
aria-label={t('admin.addons.search')}
className="h-9 w-full rounded-[10px] border border-edge-faint bg-surface-card ps-8 pe-3 text-content outline-none transition-colors placeholder:text-content-faint focus:border-accent"
style={fs(13, 'body')}
/>
</label>
<div className="inline-flex flex-none gap-1 rounded-[10px] bg-surface-tertiary p-1" role="group" aria-label={t('admin.addons.filter')}>
{(['all', 'on', 'off'] as const).map(v => (
<button
key={v}
type="button"
onClick={() => onView(v)}
aria-pressed={view === v}
className={`h-7 rounded-[8px] px-3 font-medium transition-colors ${view === v ? 'bg-surface-card text-content shadow-sm' : 'text-content-muted hover:text-content'}`}
style={fs(12.5, 'body')}
>
{t(`admin.addons.filter.${v}`)}
</button>
))}
</div>
</div>
</>
)
}
+6
View File
@@ -708,6 +708,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'تم تحديث الإضافة',
'admin.addons.toast.error': 'فشل تحديث الإضافة',
'admin.addons.group.count': '{enabled} من {total} مُفعّل',
'admin.addons.search': 'البحث في الإضافات',
'admin.addons.filter': 'عرض',
'admin.addons.filter.all': 'الكل',
'admin.addons.filter.on': 'مفعّلة',
'admin.addons.filter.off': 'معطّلة',
'admin.addons.noMatch': 'لا توجد إضافة تطابق البحث أو عامل التصفية.',
'admin.addons.subOptions': '{count} خيار',
'admin.addons.subOptions.zero': 'لا توجد خيارات',
'admin.addons.subOptions.one': 'خيار واحد',
+6
View File
@@ -749,6 +749,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Əlavə yeniləndi',
'admin.addons.toast.error': 'Əlavəni yeniləmək mümkün olmadı',
'admin.addons.group.count': '{total} əlavədən {enabled} aktivdir',
'admin.addons.search': 'Əlavələrdə axtar',
'admin.addons.filter': 'Göstər',
'admin.addons.filter.all': 'Hamısı',
'admin.addons.filter.on': 'Aktiv',
'admin.addons.filter.off': 'Deaktiv',
'admin.addons.noMatch': 'Axtarışa və ya filtrə uyğun əlavə yoxdur.',
'admin.addons.subOptions': '{count} seçim',
'admin.addons.subOptions.one': '{count} seçim',
'admin.addons.subOptionsOn': '{count} aktiv',
+6
View File
@@ -657,6 +657,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Complemento atualizado',
'admin.addons.toast.error': 'Falha ao atualizar complemento',
'admin.addons.group.count': '{enabled} de {total} ativados',
'admin.addons.search': 'Pesquisar complementos',
'admin.addons.filter': 'Mostrar',
'admin.addons.filter.all': 'Todos',
'admin.addons.filter.on': 'Ativos',
'admin.addons.filter.off': 'Inativos',
'admin.addons.noMatch': 'Nenhum complemento corresponde à pesquisa ou ao filtro.',
'admin.addons.subOptions': '{count} opções',
'admin.addons.subOptions.one': '{count} opção',
'admin.addons.subOptionsOn': '{count} ativas',
+6
View File
@@ -264,6 +264,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Complement actualitzat',
'admin.addons.toast.error': "No s'ha pogut actualitzar el complement",
'admin.addons.group.count': '{enabled} de {total} actius',
'admin.addons.search': 'Cerca complements',
'admin.addons.filter': 'Mostra',
'admin.addons.filter.all': 'Tots',
'admin.addons.filter.on': 'Actius',
'admin.addons.filter.off': 'Inactius',
'admin.addons.noMatch': 'Cap complement coincideix amb la cerca o el filtre.',
'admin.addons.subOptions': '{count} opcions',
'admin.addons.subOptions.one': '{count} opció',
'admin.addons.subOptionsOn': '{count} actives',
+6
View File
@@ -657,6 +657,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Doplněk byl aktualizován',
'admin.addons.toast.error': 'Aktualizace doplňku se nezdařila',
'admin.addons.group.count': '{enabled} z {total} zapnuto',
'admin.addons.search': 'Hledat doplňky',
'admin.addons.filter': 'Zobrazit',
'admin.addons.filter.all': 'Vše',
'admin.addons.filter.on': 'Zapnuté',
'admin.addons.filter.off': 'Vypnuté',
'admin.addons.noMatch': 'Hledání ani filtru neodpovídá žádný doplněk.',
'admin.addons.subOptions': '{count} možností',
'admin.addons.subOptions.one': '{count} možnost',
'admin.addons.subOptions.few': '{count} možnosti',
+6
View File
@@ -665,6 +665,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Addon aktualisiert',
'admin.addons.toast.error': 'Addon konnte nicht aktualisiert werden',
'admin.addons.group.count': '{enabled} von {total} aktiviert',
'admin.addons.search': 'Addons durchsuchen',
'admin.addons.filter': 'Anzeigen',
'admin.addons.filter.all': 'Alle',
'admin.addons.filter.on': 'An',
'admin.addons.filter.off': 'Aus',
'admin.addons.noMatch': 'Kein Addon passt zur Suche oder zum Filter.',
'admin.addons.subOptions': '{count} Optionen',
'admin.addons.subOptions.one': '{count} Option',
'admin.addons.subOptionsOn': '{count} aktiv',
+6
View File
@@ -710,6 +710,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Addon updated',
'admin.addons.toast.error': 'Failed to update addon',
'admin.addons.group.count': '{enabled} of {total} enabled',
'admin.addons.search': 'Search addons',
'admin.addons.filter': 'Show',
'admin.addons.filter.all': 'All',
'admin.addons.filter.on': 'On',
'admin.addons.filter.off': 'Off',
'admin.addons.noMatch': 'No addon matches the search or the filter.',
'admin.addons.subOptions': '{count} options',
'admin.addons.subOptions.one': '{count} option',
'admin.addons.subOptionsOn': '{count} on',
+6
View File
@@ -646,6 +646,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Complemento actualizado',
'admin.addons.toast.error': 'No se pudo actualizar el complemento',
'admin.addons.group.count': '{enabled} de {total} activados',
'admin.addons.search': 'Buscar complementos',
'admin.addons.filter': 'Mostrar',
'admin.addons.filter.all': 'Todos',
'admin.addons.filter.on': 'Activos',
'admin.addons.filter.off': 'Inactivos',
'admin.addons.noMatch': 'Ningún complemento coincide con la búsqueda o el filtro.',
'admin.addons.subOptions': '{count} opciones',
'admin.addons.subOptions.one': '{count} opción',
'admin.addons.subOptionsOn': '{count} activas',
+6
View File
@@ -729,6 +729,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Lisamoodul uuendatud',
'admin.addons.toast.error': 'Lisamooduli uuendamine ebaõnnestus',
'admin.addons.group.count': 'Lubatud {enabled}/{total}',
'admin.addons.search': 'Otsi lisasid',
'admin.addons.filter': 'Näita',
'admin.addons.filter.all': 'Kõik',
'admin.addons.filter.on': 'Sees',
'admin.addons.filter.off': 'Väljas',
'admin.addons.noMatch': 'Otsingule ega filtrile ei vasta ükski lisa.',
'admin.addons.subOptions': '{count} valikut',
'admin.addons.subOptions.one': '{count} valik',
'admin.addons.subOptionsOn': '{count} lubatud',
+6
View File
@@ -671,6 +671,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Extension mise à jour',
'admin.addons.toast.error': "Échec de la mise à jour de l'extension",
'admin.addons.group.count': '{enabled} sur {total} activés',
'admin.addons.search': 'Rechercher des modules',
'admin.addons.filter': 'Afficher',
'admin.addons.filter.all': 'Tous',
'admin.addons.filter.on': 'Activés',
'admin.addons.filter.off': 'Désactivés',
'admin.addons.noMatch': 'Aucun module ne correspond à la recherche ou au filtre.',
'admin.addons.subOptions': '{count} options', // same-as-en
'admin.addons.subOptions.one': '{count} option', // same-as-en
'admin.addons.subOptionsOn': '{count} activées',
+6
View File
@@ -733,6 +733,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Το πρόσθετο ενημερώθηκε',
'admin.addons.toast.error': 'Αποτυχία ενημέρωσης προσθέτου',
'admin.addons.group.count': '{enabled} από {total} ενεργοποιημένα',
'admin.addons.search': 'Αναζήτηση πρόσθετων',
'admin.addons.filter': 'Εμφάνιση',
'admin.addons.filter.all': 'Όλα',
'admin.addons.filter.on': 'Ενεργά',
'admin.addons.filter.off': 'Ανενεργά',
'admin.addons.noMatch': 'Κανένα πρόσθετο δεν ταιριάζει με την αναζήτηση ή το φίλτρο.',
'admin.addons.subOptions': '{count} επιλογές',
'admin.addons.subOptions.one': '{count} επιλογή',
'admin.addons.subOptionsOn': '{count} ενεργές',
+6
View File
@@ -671,6 +671,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Bővítmény frissítve',
'admin.addons.toast.error': 'Nem sikerült frissíteni a bővítményt',
'admin.addons.group.count': '{total} közül {enabled} bekapcsolva',
'admin.addons.search': 'Bővítmények keresése',
'admin.addons.filter': 'Megjelenítés',
'admin.addons.filter.all': 'Mind',
'admin.addons.filter.on': 'Be',
'admin.addons.filter.off': 'Ki',
'admin.addons.noMatch': 'Egy bővítmény sem felel meg a keresésnek vagy a szűrőnek.',
'admin.addons.subOptions': '{count} beállítás',
'admin.addons.subOptions.one': '{count} beállítás',
'admin.addons.subOptionsOn': '{count} bekapcsolva',
+6
View File
@@ -711,6 +711,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Addon diperbarui',
'admin.addons.toast.error': 'Gagal memperbarui addon',
'admin.addons.group.count': '{enabled} dari {total} aktif',
'admin.addons.search': 'Cari add-on',
'admin.addons.filter': 'Tampilkan',
'admin.addons.filter.all': 'Semua',
'admin.addons.filter.on': 'Aktif',
'admin.addons.filter.off': 'Nonaktif',
'admin.addons.noMatch': 'Tidak ada add-on yang cocok dengan pencarian atau filter.',
'admin.addons.subOptions': '{count} opsi',
'admin.addons.subOptionsOn': '{count} aktif',
'admin.toursWanderer.title': 'wanderer',
+6
View File
@@ -665,6 +665,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Modulo aggiornato',
'admin.addons.toast.error': 'Impossibile aggiornare il modulo',
'admin.addons.group.count': '{enabled} di {total} attivi',
'admin.addons.search': 'Cerca componenti aggiuntivi',
'admin.addons.filter': 'Mostra',
'admin.addons.filter.all': 'Tutti',
'admin.addons.filter.on': 'Attivi',
'admin.addons.filter.off': 'Disattivi',
'admin.addons.noMatch': 'Nessun componente corrisponde alla ricerca o al filtro.',
'admin.addons.subOptions': '{count} opzioni',
'admin.addons.subOptions.one': '{count} opzione',
'admin.addons.subOptionsOn': '{count} attive',
+6
View File
@@ -674,6 +674,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'アドオンを更新しました',
'admin.addons.toast.error': 'アドオンの更新に失敗しました',
'admin.addons.group.count': '{total} 件中 {enabled} 件が有効',
'admin.addons.search': 'アドオンを検索',
'admin.addons.filter': '表示',
'admin.addons.filter.all': 'すべて',
'admin.addons.filter.on': 'オン',
'admin.addons.filter.off': 'オフ',
'admin.addons.noMatch': '検索またはフィルターに一致するアドオンはありません。',
'admin.addons.subOptions': '{count}件のオプション',
'admin.addons.subOptionsOn': '{count}件が有効',
'admin.toursWanderer.title': 'wanderer',
+6
View File
@@ -673,6 +673,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': '애드온이 업데이트되었습니다',
'admin.addons.toast.error': '애드온 업데이트 실패',
'admin.addons.group.count': '{total}개 중 {enabled}개 활성화됨',
'admin.addons.search': '애드온 검색',
'admin.addons.filter': '표시',
'admin.addons.filter.all': '전체',
'admin.addons.filter.on': '켜짐',
'admin.addons.filter.off': '꺼짐',
'admin.addons.noMatch': '검색 또는 필터와 일치하는 애드온이 없습니다.',
'admin.addons.subOptions': '옵션 {count}개',
'admin.addons.subOptionsOn': '{count}개 켜짐',
'admin.toursWanderer.title': 'wanderer',
+6
View File
@@ -664,6 +664,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Add-on bijgewerkt',
'admin.addons.toast.error': 'Add-on bijwerken mislukt',
'admin.addons.group.count': '{enabled} van {total} ingeschakeld',
'admin.addons.search': 'Add-ons zoeken',
'admin.addons.filter': 'Tonen',
'admin.addons.filter.all': 'Alle',
'admin.addons.filter.on': 'Aan',
'admin.addons.filter.off': 'Uit',
'admin.addons.noMatch': 'Geen add-on past bij de zoekopdracht of het filter.',
'admin.addons.subOptions': '{count} opties',
'admin.addons.subOptions.one': '{count} optie',
'admin.addons.subOptionsOn': '{count} aan',
+6
View File
@@ -680,6 +680,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Dodatek został zaktualizowany',
'admin.addons.toast.error': 'Nie udało się zaktualizować dodatku',
'admin.addons.group.count': '{enabled} z {total} włączonych',
'admin.addons.search': 'Szukaj dodatków',
'admin.addons.filter': 'Pokaż',
'admin.addons.filter.all': 'Wszystkie',
'admin.addons.filter.on': 'Włączone',
'admin.addons.filter.off': 'Wyłączone',
'admin.addons.noMatch': 'Żaden dodatek nie pasuje do wyszukiwania ani filtra.',
'admin.addons.subOptions': '{count} opcji',
'admin.addons.subOptions.one': '{count} opcja',
'admin.addons.subOptions.few': '{count} opcje',
+6
View File
@@ -685,6 +685,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Дополнение обновлено',
'admin.addons.toast.error': 'Не удалось обновить дополнение',
'admin.addons.group.count': 'Включено {enabled} из {total}',
'admin.addons.search': 'Поиск дополнений',
'admin.addons.filter': 'Показать',
'admin.addons.filter.all': 'Все',
'admin.addons.filter.on': 'Включённые',
'admin.addons.filter.off': 'Выключенные',
'admin.addons.noMatch': 'Ни одно дополнение не подходит под поиск или фильтр.',
'admin.addons.subOptions': '{count} параметра',
'admin.addons.subOptions.one': '{count} параметр',
'admin.addons.subOptions.few': '{count} параметра',
+6
View File
@@ -734,6 +734,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Doplnok aktualizovaný',
'admin.addons.toast.error': 'Aktualizácia doplnku zlyhala',
'admin.addons.group.count': '{enabled} z {total} zapnuté',
'admin.addons.search': 'Hľadať doplnky',
'admin.addons.filter': 'Zobraziť',
'admin.addons.filter.all': 'Všetky',
'admin.addons.filter.on': 'Zapnuté',
'admin.addons.filter.off': 'Vypnuté',
'admin.addons.noMatch': 'Vyhľadávaniu ani filtru nezodpovedá žiadny doplnok.',
'admin.addons.subOptions': '{count} možností',
'admin.addons.subOptions.one': '{count} možnosť',
'admin.addons.subOptions.few': '{count} možnosti',
+6
View File
@@ -732,6 +732,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Tillägget har uppdaterats',
'admin.addons.toast.error': 'Det gick inte att uppdatera tillägget',
'admin.addons.group.count': '{enabled} av {total} aktiverade',
'admin.addons.search': 'Sök tillägg',
'admin.addons.filter': 'Visa',
'admin.addons.filter.all': 'Alla',
'admin.addons.filter.on': 'På',
'admin.addons.filter.off': 'Av',
'admin.addons.noMatch': 'Inget tillägg matchar sökningen eller filtret.',
'admin.addons.subOptions': '{count} alternativ',
'admin.addons.subOptions.one': '{count} alternativ',
'admin.addons.subOptionsOn': '{count} aktiva',
+6
View File
@@ -631,6 +631,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'อัพเดตแอดออนแล้ว',
'admin.addons.toast.error': 'ไม่สามารถอัปเดต addon',
'admin.addons.group.count': 'เปิดใช้งาน {enabled} จาก {total} แล้ว',
'admin.addons.search': 'ค้นหาส่วนเสริม',
'admin.addons.filter': 'แสดง',
'admin.addons.filter.all': 'ทั้งหมด',
'admin.addons.filter.on': 'เปิด',
'admin.addons.filter.off': 'ปิด',
'admin.addons.noMatch': 'ไม่มีส่วนเสริมที่ตรงกับการค้นหาหรือตัวกรอง',
'admin.addons.subOptions': '{count} ตัวเลือก',
'admin.addons.subOptionsOn': 'เปิดอยู่ {count}',
'admin.toursWanderer.title': 'wanderer',
+6
View File
@@ -720,6 +720,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Eklenti güncellendi',
'admin.addons.toast.error': 'Eklenti güncellenemedi',
'admin.addons.group.count': '{total} eklentiden {enabled} tanesi etkin',
'admin.addons.search': 'Eklentilerde ara',
'admin.addons.filter': 'Göster',
'admin.addons.filter.all': 'Tümü',
'admin.addons.filter.on': 'Açık',
'admin.addons.filter.off': 'Kapalı',
'admin.addons.noMatch': 'Aramaya veya filtreye uyan eklenti yok.',
'admin.addons.subOptions': '{count} seçenek',
'admin.addons.subOptions.one': '{count} seçenek',
'admin.addons.subOptionsOn': '{count} açık',
+6
View File
@@ -682,6 +682,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Доповнення оновлено',
'admin.addons.toast.error': 'Не вдалося оновити доповнення',
'admin.addons.group.count': 'Увімкнено {enabled} з {total}',
'admin.addons.search': 'Пошук доповнень',
'admin.addons.filter': 'Показати',
'admin.addons.filter.all': 'Усі',
'admin.addons.filter.on': 'Увімкнені',
'admin.addons.filter.off': 'Вимкнені',
'admin.addons.noMatch': 'Жодне доповнення не відповідає пошуку чи фільтру.',
'admin.addons.subOptions': '{count} параметра',
'admin.addons.subOptions.one': '{count} параметр',
'admin.addons.subOptions.few': '{count} параметри',
+6
View File
@@ -724,6 +724,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': 'Đã cập nhật tiện ích bổ sung',
'admin.addons.toast.error': 'Không thể cập nhật tiện ích bổ sung',
'admin.addons.group.count': '{enabled} trên {total} đã bật',
'admin.addons.search': 'Tìm tiện ích',
'admin.addons.filter': 'Hiển thị',
'admin.addons.filter.all': 'Tất cả',
'admin.addons.filter.on': 'Bật',
'admin.addons.filter.off': 'Tắt',
'admin.addons.noMatch': 'Không có tiện ích nào khớp với tìm kiếm hoặc bộ lọc.',
'admin.addons.subOptions': '{count} tùy chọn',
'admin.addons.subOptionsOn': '{count} đang bật',
'admin.toursWanderer.title': 'wanderer',
+6
View File
@@ -645,6 +645,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': '擴充套件已更新',
'admin.addons.toast.error': '更新擴充套件失敗',
'admin.addons.group.count': '{total} 項中已啟用 {enabled} 項',
'admin.addons.search': '搜尋外掛',
'admin.addons.filter': '顯示',
'admin.addons.filter.all': '全部',
'admin.addons.filter.on': '已開啟',
'admin.addons.filter.off': '已關閉',
'admin.addons.noMatch': '沒有外掛符合搜尋或篩選條件。',
'admin.addons.subOptions': '{count} 個選項',
'admin.addons.subOptionsOn': '已開啟 {count} 個',
'admin.toursWanderer.title': 'wanderer',
+6
View File
@@ -596,6 +596,12 @@ const admin: TranslationStrings = {
'admin.addons.toast.updated': '扩展已更新',
'admin.addons.toast.error': '更新扩展失败',
'admin.addons.group.count': '{total} 项中已启用 {enabled} 项',
'admin.addons.search': '搜索插件',
'admin.addons.filter': '显示',
'admin.addons.filter.all': '全部',
'admin.addons.filter.on': '已开启',
'admin.addons.filter.off': '已关闭',
'admin.addons.noMatch': '没有插件符合搜索或筛选条件。',
'admin.addons.subOptions': '{count} 个选项',
'admin.addons.subOptionsOn': '已开启 {count} 个',
'admin.toursWanderer.title': 'wanderer',