mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(collections): select toolbar — select-all, move/duplicate to another list (#1081)
- The select toggle now sits at the right of the filter row (same height as the status/category dropdowns) instead of the top toolbar. - Select mode gains a "select all / deselect all" toggle and shows even with nothing selected yet. - Selected places can be moved or duplicated into another of your lists via a target-list picker (move re-points collection_id; duplicate re-saves the place data, carrying description / category / notes / etc.). - New strings across all 22 locales.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { ChevronDown, Check, Layers, Tag } from 'lucide-react'
|
||||
import { ChevronDown, Check, Layers, Tag, CheckSquare } from 'lucide-react'
|
||||
import type { StatusFilter } from '../../store/collectionStore'
|
||||
import type { TranslationFn } from '../../types'
|
||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||
@@ -69,6 +69,9 @@ interface CollectionFilterBarProps {
|
||||
categoryOptions: CategoryOption[]
|
||||
onStatusFilter: (f: StatusFilter) => void
|
||||
onCategoryFilter: (f: number | 'all') => void
|
||||
showSelect: boolean
|
||||
selectMode: boolean
|
||||
onToggleSelect: () => void
|
||||
t: TranslationFn
|
||||
}
|
||||
|
||||
@@ -78,7 +81,8 @@ interface CollectionFilterBarProps {
|
||||
* Custom compact dropdowns so they barely take any space.
|
||||
*/
|
||||
export default function CollectionFilterBar({
|
||||
statusFilter, counts, categoryFilter, categoryOptions, onStatusFilter, onCategoryFilter, t,
|
||||
statusFilter, counts, categoryFilter, categoryOptions, onStatusFilter, onCategoryFilter,
|
||||
showSelect, selectMode, onToggleSelect, t,
|
||||
}: CollectionFilterBarProps): React.ReactElement {
|
||||
const statusOpts: Opt[] = [
|
||||
{ key: 'all', label: t('common.all'), count: counts.all },
|
||||
@@ -103,6 +107,11 @@ export default function CollectionFilterBar({
|
||||
{categoryOptions.length > 0 && (
|
||||
<Dropdown current={categoryFilter} options={catOpts} onSelect={k => onCategoryFilter(k as number | 'all')} lead={<Tag size={13} />} />
|
||||
)}
|
||||
{showSelect && (
|
||||
<button type="button" onClick={onToggleSelect} className={`col-filter-btn col-filter-select${selectMode ? ' open' : ''}`} aria-pressed={selectMode}>
|
||||
<CheckSquare size={14} /> <span className="col-filter-lbl">{t('collections.select')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Search, ArrowRight, Copy, Loader2, Bookmark } from 'lucide-react'
|
||||
import Modal from '../shared/Modal'
|
||||
import type { Collection } from '@trek/shared'
|
||||
import type { TranslationFn } from '../../types'
|
||||
|
||||
interface MoveToListModalProps {
|
||||
mode: 'move' | 'copy'
|
||||
/** Candidate target lists (owned, excluding the current one). */
|
||||
lists: Collection[]
|
||||
/** Number of selected places. */
|
||||
count: number
|
||||
onPick: (targetId: number) => Promise<void> | void
|
||||
onClose: () => void
|
||||
t: TranslationFn
|
||||
}
|
||||
|
||||
/**
|
||||
* Target-list picker for moving or duplicating the selected places into another
|
||||
* of the user's lists. `mode` only changes the wording + the trailing icon; the
|
||||
* action itself is the parent's onPick.
|
||||
*/
|
||||
export default function MoveToListModal({ mode, lists, count, onPick, onClose, t }: MoveToListModalProps): React.ReactElement {
|
||||
const [search, setSearch] = useState('')
|
||||
const [busy, setBusy] = useState<number | null>(null)
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase()
|
||||
return q ? lists.filter(l => l.name.toLowerCase().includes(q)) : lists
|
||||
}, [lists, search])
|
||||
|
||||
const pick = async (id: number) => {
|
||||
if (busy != null) return
|
||||
setBusy(id)
|
||||
try { await onPick(id) } finally { setBusy(null) }
|
||||
}
|
||||
|
||||
const Trailing = mode === 'move' ? ArrowRight : Copy
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
title={mode === 'move' ? t('collections.moveToListTitle', { count }) : t('collections.duplicateToListTitle', { count })}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{lists.length > 3 && (
|
||||
<div className="relative">
|
||||
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-content-faint" />
|
||||
<input
|
||||
autoFocus
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={t('collections.copyToTripSearch')}
|
||||
className="w-full pl-8 pr-3 py-2 rounded-lg border border-edge bg-surface-input text-content text-[13px] outline-none focus:border-accent"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filtered.length === 0 ? (
|
||||
<p className="text-center text-[13px] text-content-faint py-8">{t('collections.noOtherLists')}</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1 max-h-[50vh] overflow-y-auto -mx-1 px-1">
|
||||
{filtered.map(list => {
|
||||
const isBusy = busy === list.id
|
||||
return (
|
||||
<button
|
||||
key={list.id}
|
||||
type="button"
|
||||
onClick={() => pick(list.id)}
|
||||
disabled={busy != null}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-xl border border-edge bg-surface-card text-left hover:bg-surface-hover transition-colors disabled:opacity-60"
|
||||
>
|
||||
<span className="w-9 h-9 min-w-[36px] rounded-lg flex items-center justify-center shrink-0 text-white" style={{ background: list.color || '#6366f1' }}>
|
||||
<Bookmark size={15} />
|
||||
</span>
|
||||
<span className="flex-1 min-w-0">
|
||||
<span className="block text-[13px] font-semibold text-content truncate">{list.name}</span>
|
||||
<span className="block text-[11.5px] text-content-faint">{t('collections.placeCount', { count: list.place_count ?? 0 })}</span>
|
||||
</span>
|
||||
{isBusy ? <Loader2 size={15} className="animate-spin text-content-faint shrink-0" /> : <Trailing size={15} className="text-content-faint shrink-0" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { List as ListIcon, Map as MapIcon, Search, Bookmark, CheckSquare, X, Trash2, Copy, Plus } from 'lucide-react'
|
||||
import { List as ListIcon, Map as MapIcon, Search, Bookmark, CheckCheck, X, Trash2, Copy, CopyPlus, FolderInput, Plus } from 'lucide-react'
|
||||
import Navbar from '../components/Layout/Navbar'
|
||||
import Modal from '../components/shared/Modal'
|
||||
import ListsRail from '../components/Collections/ListsRail'
|
||||
@@ -9,6 +9,7 @@ import CollectionList from '../components/Collections/CollectionList'
|
||||
import CollectionFilterBar from '../components/Collections/CollectionFilterBar'
|
||||
import CollectionMapPanel from '../components/Collections/CollectionMapPanel'
|
||||
import CopyToTripModal from '../components/Collections/CopyToTripModal'
|
||||
import MoveToListModal from '../components/Collections/MoveToListModal'
|
||||
import ShareCollectionModal from '../components/Collections/ShareCollectionModal'
|
||||
import AddPlaceToCollectionModal from '../components/Collections/AddPlaceToCollectionModal'
|
||||
import CollectionPlaceDetail from '../components/Collections/CollectionPlaceDetail'
|
||||
@@ -105,6 +106,9 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
categoryOptions={c.categoryOptions}
|
||||
onStatusFilter={c.setStatusFilter}
|
||||
onCategoryFilter={c.setCategoryFilter}
|
||||
showSelect={showSelect}
|
||||
selectMode={c.selectMode}
|
||||
onToggleSelect={() => c.setSelectMode(!c.selectMode)}
|
||||
t={t}
|
||||
/>
|
||||
) : null
|
||||
@@ -204,17 +208,6 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showSelect && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => c.setSelectMode(!c.selectMode)}
|
||||
className={`col-iconbtn${c.selectMode ? ' on' : ''}`}
|
||||
aria-label={t('collections.selectMode')}
|
||||
title={t('collections.selectMode')}
|
||||
>
|
||||
<CheckSquare size={16} />
|
||||
</button>
|
||||
)}
|
||||
{canAddPlace && (
|
||||
<button type="button" onClick={() => c.setShowAddPlace(true)} className="col-iconbtn" aria-label={t('collections.addPlace')} title={t('collections.addPlace')}>
|
||||
<Plus size={16} />
|
||||
@@ -234,14 +227,23 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{c.selectMode && c.selectedIds.length > 0 && (
|
||||
{c.selectMode && (
|
||||
<div className="col-selbar">
|
||||
<button type="button" onClick={c.handleSelectAll} className="col-selbar-btn">
|
||||
<CheckCheck size={14} /> {c.allVisibleSelected ? t('collections.deselectAll') : t('collections.selectAll')}
|
||||
</button>
|
||||
<span className="lbl">{t('collections.selectedCount', { count: c.selectedIds.length })}</span>
|
||||
<div className="col-toolbar-spacer" />
|
||||
<button type="button" onClick={c.openCopyForSelection} className="col-selbar-btn">
|
||||
<Copy size={14} /> {t('collections.copyN', { count: c.selectedIds.length })}
|
||||
<button type="button" onClick={() => c.setListPickerMode('move')} disabled={c.selectedIds.length === 0} className="col-selbar-btn">
|
||||
<FolderInput size={14} /> {t('collections.moveToList')}
|
||||
</button>
|
||||
<button type="button" onClick={c.handleDeleteSelected} className="col-selbar-btn danger">
|
||||
<button type="button" onClick={() => c.setListPickerMode('copy')} disabled={c.selectedIds.length === 0} className="col-selbar-btn">
|
||||
<CopyPlus size={14} /> {t('collections.duplicateToList')}
|
||||
</button>
|
||||
<button type="button" onClick={c.openCopyForSelection} disabled={c.selectedIds.length === 0} className="col-selbar-btn">
|
||||
<Copy size={14} /> {t('collections.copyToTrip')}
|
||||
</button>
|
||||
<button type="button" onClick={c.handleDeleteSelected} disabled={c.selectedIds.length === 0} className="col-selbar-btn danger">
|
||||
<Trash2 size={14} /> {t('common.delete')}
|
||||
</button>
|
||||
<button type="button" onClick={() => c.setSelectMode(false)} className="col-selbar-btn" aria-label={t('common.cancel')}>
|
||||
@@ -306,6 +308,18 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Move / duplicate the selection into another list */}
|
||||
{c.listPickerMode && (
|
||||
<MoveToListModal
|
||||
mode={c.listPickerMode}
|
||||
lists={c.ownedLists.filter(l => l.id !== c.activeId)}
|
||||
count={c.selectedIds.length}
|
||||
onPick={c.listPickerMode === 'move' ? c.handleMoveToList : c.handleDuplicateToList}
|
||||
onClose={() => c.setListPickerMode(null)}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Share / fusion */}
|
||||
{c.canShare && typeof c.activeId === 'number' && c.activeCollection && (
|
||||
<ShareCollectionModal
|
||||
|
||||
@@ -53,6 +53,7 @@ export function useCollections() {
|
||||
loadAll, setActive, refreshActive, loadCollection,
|
||||
deleteCollection,
|
||||
setStatus, updatePlace, deletePlace, deleteMany, copyToTrip, clearSelection,
|
||||
moveToList, duplicateToList, setSelectedIds,
|
||||
acceptInvite, declineInvite,
|
||||
setView, setStatusFilter, setCategoryFilter, setSearch, setSelectedPlaceId, setSelectMode, toggleSelect,
|
||||
} = store
|
||||
@@ -211,6 +212,35 @@ export function useCollections() {
|
||||
}
|
||||
}, [selectedIds, deleteMany, toast, t])
|
||||
|
||||
// Select-all toggles between every visible place and none.
|
||||
const allVisibleSelected = visiblePlaces.length > 0 && visiblePlaces.every(p => selectedIds.includes(p.id))
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedIds(allVisibleSelected ? [] : visiblePlaces.map(p => p.id))
|
||||
}, [allVisibleSelected, visiblePlaces, setSelectedIds])
|
||||
|
||||
// Move / duplicate the selection into another list (a list-picker modal).
|
||||
const [listPickerMode, setListPickerMode] = useState<'move' | 'copy' | null>(null)
|
||||
const handleMoveToList = useCallback(async (targetId: number) => {
|
||||
if (selectedIds.length === 0) return
|
||||
try {
|
||||
await moveToList(selectedIds, targetId)
|
||||
toast.success(t('collections.movedCount', { count: selectedIds.length }))
|
||||
setListPickerMode(null)
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||
}
|
||||
}, [selectedIds, moveToList, toast, t])
|
||||
const handleDuplicateToList = useCallback(async (targetId: number) => {
|
||||
if (selectedIds.length === 0) return
|
||||
try {
|
||||
await duplicateToList(selectedIds, targetId)
|
||||
toast.success(t('collections.duplicatedCount', { count: selectedIds.length }))
|
||||
setListPickerMode(null)
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||
}
|
||||
}, [selectedIds, duplicateToList, toast, t])
|
||||
|
||||
const handleAcceptInvite = useCallback(async (collectionId: number) => {
|
||||
try {
|
||||
await acceptInvite(collectionId)
|
||||
@@ -311,5 +341,7 @@ export function useCollections() {
|
||||
handleSelectList, handleDeleteList,
|
||||
handleStatusChange, handleDeletePlace, handleDeleteSelected,
|
||||
handleAcceptInvite, handleDeclineInvite,
|
||||
allVisibleSelected, handleSelectAll,
|
||||
listPickerMode, setListPickerMode, handleMoveToList, handleDuplicateToList,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ interface CollectionState {
|
||||
deletePlace: (placeId: number) => Promise<void>
|
||||
deleteMany: (ids: number[]) => Promise<void>
|
||||
copyToTrip: (tripId: number, placeIds: number[], force?: boolean) => Promise<{ copied: number; skipped: { id: number; name: string }[] }>
|
||||
moveToList: (placeIds: number[], targetId: number) => Promise<void>
|
||||
duplicateToList: (placeIds: number[], targetId: number) => Promise<void>
|
||||
|
||||
invite: (collectionId: number, userId: number) => Promise<void>
|
||||
acceptInvite: (collectionId: number) => Promise<void>
|
||||
@@ -68,6 +70,7 @@ interface CollectionState {
|
||||
setSelectedPlaceId: (id: number | null) => void
|
||||
setSelectMode: (on: boolean) => void
|
||||
toggleSelect: (id: number) => void
|
||||
setSelectedIds: (ids: number[]) => void
|
||||
clearSelection: () => void
|
||||
}
|
||||
|
||||
@@ -226,6 +229,49 @@ export const useCollectionStore = create<CollectionState>((set, get) => ({
|
||||
return res
|
||||
},
|
||||
|
||||
// Move the selected places into another list (re-point collection_id). They
|
||||
// leave the current list, so drop them locally + refresh.
|
||||
moveToList: async (placeIds: number[], targetId: number) => {
|
||||
for (const id of placeIds) await collectionsApi.updatePlace(id, { collection_id: targetId })
|
||||
const idSet = new Set(placeIds)
|
||||
set({ places: get().places.filter(p => !idSet.has(p.id)), selectedIds: [], selectMode: false })
|
||||
await get().loadAll()
|
||||
const active = get().activeId
|
||||
if (typeof active === 'number') await get().loadCollection(active)
|
||||
},
|
||||
|
||||
// Duplicate the selected places into another list (re-save each place's data).
|
||||
duplicateToList: async (placeIds: number[], targetId: number) => {
|
||||
const byId = new Map(get().places.map(p => [p.id, p]))
|
||||
for (const id of placeIds) {
|
||||
const p = byId.get(id)
|
||||
if (!p) continue
|
||||
await collectionsApi.savePlace({
|
||||
collection_id: targetId,
|
||||
name: p.name,
|
||||
description: p.description ?? null,
|
||||
lat: p.lat ?? null,
|
||||
lng: p.lng ?? null,
|
||||
address: p.address ?? null,
|
||||
category_id: p.category_id ?? null,
|
||||
price: p.price ?? null,
|
||||
currency: p.currency ?? null,
|
||||
notes: p.notes ?? null,
|
||||
image_url: p.image_url ?? null,
|
||||
google_place_id: p.google_place_id ?? null,
|
||||
google_ftid: p.google_ftid ?? null,
|
||||
osm_id: p.osm_id ?? null,
|
||||
website: p.website ?? null,
|
||||
phone: p.phone ?? null,
|
||||
status: p.status,
|
||||
links: p.links ?? [],
|
||||
force: true,
|
||||
})
|
||||
}
|
||||
set({ selectedIds: [], selectMode: false })
|
||||
await get().loadAll()
|
||||
},
|
||||
|
||||
invite: async (collectionId: number, userId: number) => {
|
||||
await collectionsApi.invite(collectionId, userId)
|
||||
if (get().activeId === collectionId) await get().loadCollection(collectionId)
|
||||
@@ -267,5 +313,6 @@ export const useCollectionStore = create<CollectionState>((set, get) => ({
|
||||
const selected = get().selectedIds
|
||||
set({ selectedIds: selected.includes(id) ? selected.filter(x => x !== id) : [...selected, id] })
|
||||
},
|
||||
setSelectedIds: (ids: number[]) => set({ selectedIds: ids }),
|
||||
clearSelection: () => set({ selectedIds: [], selectMode: false }),
|
||||
}))
|
||||
|
||||
@@ -273,6 +273,9 @@
|
||||
/* ----------------- filter bar (status + category dropdowns) ----------------- */
|
||||
.trek-dash .col-filterbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
||||
.trek-dash .col-filter { position: relative; }
|
||||
.trek-dash .col-filter-select { margin-left: auto; }
|
||||
.trek-dash .col-filter-select.open { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.trek-dash .col-filter-select.open:hover { background: var(--accent); color: #fff; }
|
||||
.trek-dash .col-filter-btn { display: inline-flex; align-items: center; gap: 7px; padding: 7px 11px; border-radius: 10px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); background: var(--surface); border: 1px solid var(--line-2); transition: background .12s, border-color .12s, color .12s; }
|
||||
.trek-dash .col-filter-btn:hover { background: var(--surface-2); color: var(--ink); }
|
||||
.trek-dash .col-filter-btn.open { border-color: var(--accent); color: var(--ink); }
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'تحديد',
|
||||
'collections.selectAll': 'تحديد الكل',
|
||||
'collections.deselectAll': 'إلغاء التحديد',
|
||||
'collections.moveToList': 'نقل إلى قائمة',
|
||||
'collections.duplicateToList': 'تكرار إلى قائمة',
|
||||
'collections.moveToListTitle': 'نقل {count} إلى قائمة أخرى',
|
||||
'collections.duplicateToListTitle': 'تكرار {count} إلى قائمة أخرى',
|
||||
'collections.noOtherLists': 'لا توجد قوائم أخرى بعد',
|
||||
'collections.movedCount': 'تم نقل {count} أماكن',
|
||||
'collections.duplicatedCount': 'تم تكرار {count} أماكن',
|
||||
'collections.placeCount': '{count} أماكن',
|
||||
'collections.category': 'الفئة',
|
||||
'collections.noCategory': 'بدون فئة',
|
||||
'collections.editListTitle': 'تعديل القائمة',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Selecionar',
|
||||
'collections.selectAll': 'Selecionar tudo',
|
||||
'collections.deselectAll': 'Desmarcar tudo',
|
||||
'collections.moveToList': 'Mover para lista',
|
||||
'collections.duplicateToList': 'Duplicar em lista',
|
||||
'collections.moveToListTitle': 'Mover {count} para outra lista',
|
||||
'collections.duplicateToListTitle': 'Duplicar {count} em outra lista',
|
||||
'collections.noOtherLists': 'Nenhuma outra lista ainda',
|
||||
'collections.movedCount': '{count} lugares movidos',
|
||||
'collections.duplicatedCount': '{count} lugares duplicados',
|
||||
'collections.placeCount': '{count} lugares',
|
||||
'collections.category': 'Categoria',
|
||||
'collections.noCategory': 'Sem categoria',
|
||||
'collections.editListTitle': 'Editar lista',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Vybrat',
|
||||
'collections.selectAll': 'Vybrat vše',
|
||||
'collections.deselectAll': 'Zrušit výběr',
|
||||
'collections.moveToList': 'Přesunout do seznamu',
|
||||
'collections.duplicateToList': 'Duplikovat do seznamu',
|
||||
'collections.moveToListTitle': 'Přesunout {count} do jiného seznamu',
|
||||
'collections.duplicateToListTitle': 'Duplikovat {count} do jiného seznamu',
|
||||
'collections.noOtherLists': 'Zatím žádné další seznamy',
|
||||
'collections.movedCount': 'Přesunuto {count} míst',
|
||||
'collections.duplicatedCount': 'Duplikováno {count} míst',
|
||||
'collections.placeCount': '{count} míst',
|
||||
'collections.category': 'Kategorie',
|
||||
'collections.noCategory': 'Bez kategorie',
|
||||
'collections.editListTitle': 'Upravit seznam',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Auswählen',
|
||||
'collections.selectAll': 'Alle auswählen',
|
||||
'collections.deselectAll': 'Auswahl aufheben',
|
||||
'collections.moveToList': 'In Liste verschieben',
|
||||
'collections.duplicateToList': 'In Liste duplizieren',
|
||||
'collections.moveToListTitle': '{count} in andere Liste verschieben',
|
||||
'collections.duplicateToListTitle': '{count} in andere Liste duplizieren',
|
||||
'collections.noOtherLists': 'Noch keine anderen Listen',
|
||||
'collections.movedCount': '{count} Orte verschoben',
|
||||
'collections.duplicatedCount': '{count} Orte dupliziert',
|
||||
'collections.placeCount': '{count} Orte',
|
||||
'collections.category': 'Kategorie',
|
||||
'collections.noCategory': 'Keine Kategorie',
|
||||
'collections.editListTitle': 'Liste bearbeiten',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Select',
|
||||
'collections.selectAll': 'Select all',
|
||||
'collections.deselectAll': 'Deselect all',
|
||||
'collections.moveToList': 'Move to list',
|
||||
'collections.duplicateToList': 'Duplicate to list',
|
||||
'collections.moveToListTitle': 'Move {count} to another list',
|
||||
'collections.duplicateToListTitle': 'Duplicate {count} to another list',
|
||||
'collections.noOtherLists': 'No other lists yet',
|
||||
'collections.movedCount': 'Moved {count} places',
|
||||
'collections.duplicatedCount': 'Duplicated {count} places',
|
||||
'collections.placeCount': '{count} places',
|
||||
'collections.category': 'Category',
|
||||
'collections.noCategory': 'No category',
|
||||
'collections.editListTitle': 'Edit list',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Seleccionar',
|
||||
'collections.selectAll': 'Seleccionar todo',
|
||||
'collections.deselectAll': 'Deseleccionar todo',
|
||||
'collections.moveToList': 'Mover a lista',
|
||||
'collections.duplicateToList': 'Duplicar en lista',
|
||||
'collections.moveToListTitle': 'Mover {count} a otra lista',
|
||||
'collections.duplicateToListTitle': 'Duplicar {count} en otra lista',
|
||||
'collections.noOtherLists': 'Aún no hay otras listas',
|
||||
'collections.movedCount': 'Movidos {count} lugares',
|
||||
'collections.duplicatedCount': 'Duplicados {count} lugares',
|
||||
'collections.placeCount': '{count} lugares',
|
||||
'collections.category': 'Categoría',
|
||||
'collections.noCategory': 'Sin categoría',
|
||||
'collections.editListTitle': 'Editar lista',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Sélectionner',
|
||||
'collections.selectAll': 'Tout sélectionner',
|
||||
'collections.deselectAll': 'Tout désélectionner',
|
||||
'collections.moveToList': 'Déplacer vers une liste',
|
||||
'collections.duplicateToList': 'Dupliquer dans une liste',
|
||||
'collections.moveToListTitle': 'Déplacer {count} vers une autre liste',
|
||||
'collections.duplicateToListTitle': 'Dupliquer {count} dans une autre liste',
|
||||
'collections.noOtherLists': 'Aucune autre liste',
|
||||
'collections.movedCount': '{count} lieux déplacés',
|
||||
'collections.duplicatedCount': '{count} lieux dupliqués',
|
||||
'collections.placeCount': '{count} lieux',
|
||||
'collections.category': 'Catégorie',
|
||||
'collections.noCategory': 'Aucune catégorie',
|
||||
'collections.editListTitle': 'Modifier la liste',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Επιλογή',
|
||||
'collections.selectAll': 'Επιλογή όλων',
|
||||
'collections.deselectAll': 'Κατάργηση επιλογής',
|
||||
'collections.moveToList': 'Μετακίνηση σε λίστα',
|
||||
'collections.duplicateToList': 'Αντιγραφή σε λίστα',
|
||||
'collections.moveToListTitle': 'Μετακίνηση {count} σε άλλη λίστα',
|
||||
'collections.duplicateToListTitle': 'Αντιγραφή {count} σε άλλη λίστα',
|
||||
'collections.noOtherLists': 'Δεν υπάρχουν άλλες λίστες',
|
||||
'collections.movedCount': 'Μετακινήθηκαν {count} τοποθεσίες',
|
||||
'collections.duplicatedCount': 'Αντιγράφηκαν {count} τοποθεσίες',
|
||||
'collections.placeCount': '{count} τοποθεσίες',
|
||||
'collections.category': 'Κατηγορία',
|
||||
'collections.noCategory': 'Χωρίς κατηγορία',
|
||||
'collections.editListTitle': 'Επεξεργασία λίστας',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Kijelölés',
|
||||
'collections.selectAll': 'Összes kijelölése',
|
||||
'collections.deselectAll': 'Kijelölés megszüntetése',
|
||||
'collections.moveToList': 'Áthelyezés listába',
|
||||
'collections.duplicateToList': 'Másolás listába',
|
||||
'collections.moveToListTitle': '{count} áthelyezése másik listába',
|
||||
'collections.duplicateToListTitle': '{count} másolása másik listába',
|
||||
'collections.noOtherLists': 'Nincs még másik lista',
|
||||
'collections.movedCount': '{count} hely áthelyezve',
|
||||
'collections.duplicatedCount': '{count} hely másolva',
|
||||
'collections.placeCount': '{count} hely',
|
||||
'collections.category': 'Kategória',
|
||||
'collections.noCategory': 'Nincs kategória',
|
||||
'collections.editListTitle': 'Lista szerkesztése',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Pilih',
|
||||
'collections.selectAll': 'Pilih semua',
|
||||
'collections.deselectAll': 'Batalkan pilihan',
|
||||
'collections.moveToList': 'Pindahkan ke daftar',
|
||||
'collections.duplicateToList': 'Duplikat ke daftar',
|
||||
'collections.moveToListTitle': 'Pindahkan {count} ke daftar lain',
|
||||
'collections.duplicateToListTitle': 'Duplikat {count} ke daftar lain',
|
||||
'collections.noOtherLists': 'Belum ada daftar lain',
|
||||
'collections.movedCount': '{count} tempat dipindahkan',
|
||||
'collections.duplicatedCount': '{count} tempat diduplikat',
|
||||
'collections.placeCount': '{count} tempat',
|
||||
'collections.category': 'Kategori',
|
||||
'collections.noCategory': 'Tanpa kategori',
|
||||
'collections.editListTitle': 'Edit daftar',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Seleziona',
|
||||
'collections.selectAll': 'Seleziona tutto',
|
||||
'collections.deselectAll': 'Deseleziona tutto',
|
||||
'collections.moveToList': 'Sposta in lista',
|
||||
'collections.duplicateToList': 'Duplica in lista',
|
||||
'collections.moveToListTitle': 'Sposta {count} in un\'altra lista',
|
||||
'collections.duplicateToListTitle': 'Duplica {count} in un\'altra lista',
|
||||
'collections.noOtherLists': 'Nessun\'altra lista',
|
||||
'collections.movedCount': 'Spostati {count} luoghi',
|
||||
'collections.duplicatedCount': 'Duplicati {count} luoghi',
|
||||
'collections.placeCount': '{count} luoghi',
|
||||
'collections.category': 'Categoria',
|
||||
'collections.noCategory': 'Nessuna categoria',
|
||||
'collections.editListTitle': 'Modifica elenco',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': '選択',
|
||||
'collections.selectAll': 'すべて選択',
|
||||
'collections.deselectAll': '選択を解除',
|
||||
'collections.moveToList': 'リストに移動',
|
||||
'collections.duplicateToList': 'リストに複製',
|
||||
'collections.moveToListTitle': '{count} 件を別のリストに移動',
|
||||
'collections.duplicateToListTitle': '{count} 件を別のリストに複製',
|
||||
'collections.noOtherLists': '他のリストはまだありません',
|
||||
'collections.movedCount': '{count} 件の場所を移動しました',
|
||||
'collections.duplicatedCount': '{count} 件の場所を複製しました',
|
||||
'collections.placeCount': '{count} 件の場所',
|
||||
'collections.category': 'カテゴリ',
|
||||
'collections.noCategory': 'カテゴリなし',
|
||||
'collections.editListTitle': 'リストを編集',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': '선택',
|
||||
'collections.selectAll': '모두 선택',
|
||||
'collections.deselectAll': '선택 해제',
|
||||
'collections.moveToList': '목록으로 이동',
|
||||
'collections.duplicateToList': '목록에 복제',
|
||||
'collections.moveToListTitle': '{count}개를 다른 목록으로 이동',
|
||||
'collections.duplicateToListTitle': '{count}개를 다른 목록에 복제',
|
||||
'collections.noOtherLists': '다른 목록이 아직 없습니다',
|
||||
'collections.movedCount': '{count}개 장소 이동됨',
|
||||
'collections.duplicatedCount': '{count}개 장소 복제됨',
|
||||
'collections.placeCount': '장소 {count}개',
|
||||
'collections.category': '카테고리',
|
||||
'collections.noCategory': '카테고리 없음',
|
||||
'collections.editListTitle': '목록 편집',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Selecteren',
|
||||
'collections.selectAll': 'Alles selecteren',
|
||||
'collections.deselectAll': 'Selectie opheffen',
|
||||
'collections.moveToList': 'Naar lijst verplaatsen',
|
||||
'collections.duplicateToList': 'Naar lijst dupliceren',
|
||||
'collections.moveToListTitle': '{count} naar andere lijst verplaatsen',
|
||||
'collections.duplicateToListTitle': '{count} naar andere lijst dupliceren',
|
||||
'collections.noOtherLists': 'Nog geen andere lijsten',
|
||||
'collections.movedCount': '{count} plaatsen verplaatst',
|
||||
'collections.duplicatedCount': '{count} plaatsen gedupliceerd',
|
||||
'collections.placeCount': '{count} plaatsen',
|
||||
'collections.category': 'Categorie',
|
||||
'collections.noCategory': 'Geen categorie',
|
||||
'collections.editListTitle': 'Lijst bewerken',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Zaznacz',
|
||||
'collections.selectAll': 'Zaznacz wszystko',
|
||||
'collections.deselectAll': 'Odznacz wszystko',
|
||||
'collections.moveToList': 'Przenieś do listy',
|
||||
'collections.duplicateToList': 'Duplikuj do listy',
|
||||
'collections.moveToListTitle': 'Przenieś {count} do innej listy',
|
||||
'collections.duplicateToListTitle': 'Duplikuj {count} do innej listy',
|
||||
'collections.noOtherLists': 'Brak innych list',
|
||||
'collections.movedCount': 'Przeniesiono {count} miejsc',
|
||||
'collections.duplicatedCount': 'Zduplikowano {count} miejsc',
|
||||
'collections.placeCount': '{count} miejsc',
|
||||
'collections.category': 'Kategoria',
|
||||
'collections.noCategory': 'Bez kategorii',
|
||||
'collections.editListTitle': 'Edytuj listę',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Выбрать',
|
||||
'collections.selectAll': 'Выбрать все',
|
||||
'collections.deselectAll': 'Снять выделение',
|
||||
'collections.moveToList': 'Переместить в список',
|
||||
'collections.duplicateToList': 'Дублировать в список',
|
||||
'collections.moveToListTitle': 'Переместить {count} в другой список',
|
||||
'collections.duplicateToListTitle': 'Дублировать {count} в другой список',
|
||||
'collections.noOtherLists': 'Других списков пока нет',
|
||||
'collections.movedCount': 'Перемещено мест: {count}',
|
||||
'collections.duplicatedCount': 'Продублировано мест: {count}',
|
||||
'collections.placeCount': '{count} мест',
|
||||
'collections.category': 'Категория',
|
||||
'collections.noCategory': 'Без категории',
|
||||
'collections.editListTitle': 'Изменить список',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Välj',
|
||||
'collections.selectAll': 'Välj alla',
|
||||
'collections.deselectAll': 'Avmarkera alla',
|
||||
'collections.moveToList': 'Flytta till lista',
|
||||
'collections.duplicateToList': 'Duplicera till lista',
|
||||
'collections.moveToListTitle': 'Flytta {count} till annan lista',
|
||||
'collections.duplicateToListTitle': 'Duplicera {count} till annan lista',
|
||||
'collections.noOtherLists': 'Inga andra listor än',
|
||||
'collections.movedCount': 'Flyttade {count} platser',
|
||||
'collections.duplicatedCount': 'Duplicerade {count} platser',
|
||||
'collections.placeCount': '{count} platser',
|
||||
'collections.category': 'Kategori',
|
||||
'collections.noCategory': 'Ingen kategori',
|
||||
'collections.editListTitle': 'Redigera lista',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Seç',
|
||||
'collections.selectAll': 'Tümünü seç',
|
||||
'collections.deselectAll': 'Seçimi kaldır',
|
||||
'collections.moveToList': 'Listeye taşı',
|
||||
'collections.duplicateToList': 'Listeye çoğalt',
|
||||
'collections.moveToListTitle': '{count} öğeyi başka listeye taşı',
|
||||
'collections.duplicateToListTitle': '{count} öğeyi başka listeye çoğalt',
|
||||
'collections.noOtherLists': 'Henüz başka liste yok',
|
||||
'collections.movedCount': '{count} yer taşındı',
|
||||
'collections.duplicatedCount': '{count} yer çoğaltıldı',
|
||||
'collections.placeCount': '{count} yer',
|
||||
'collections.category': 'Kategori',
|
||||
'collections.noCategory': 'Kategori yok',
|
||||
'collections.editListTitle': 'Listeyi düzenle',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Вибрати',
|
||||
'collections.selectAll': 'Вибрати все',
|
||||
'collections.deselectAll': 'Зняти вибір',
|
||||
'collections.moveToList': 'Перемістити до списку',
|
||||
'collections.duplicateToList': 'Дублювати до списку',
|
||||
'collections.moveToListTitle': 'Перемістити {count} до іншого списку',
|
||||
'collections.duplicateToListTitle': 'Дублювати {count} до іншого списку',
|
||||
'collections.noOtherLists': 'Інших списків поки немає',
|
||||
'collections.movedCount': 'Переміщено місць: {count}',
|
||||
'collections.duplicatedCount': 'Продубльовано місць: {count}',
|
||||
'collections.placeCount': '{count} місць',
|
||||
'collections.category': 'Категорія',
|
||||
'collections.noCategory': 'Без категорії',
|
||||
'collections.editListTitle': 'Редагувати список',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': 'Chọn',
|
||||
'collections.selectAll': 'Chọn tất cả',
|
||||
'collections.deselectAll': 'Bỏ chọn tất cả',
|
||||
'collections.moveToList': 'Chuyển vào danh sách',
|
||||
'collections.duplicateToList': 'Sao chép vào danh sách',
|
||||
'collections.moveToListTitle': 'Chuyển {count} sang danh sách khác',
|
||||
'collections.duplicateToListTitle': 'Sao chép {count} sang danh sách khác',
|
||||
'collections.noOtherLists': 'Chưa có danh sách nào khác',
|
||||
'collections.movedCount': 'Đã chuyển {count} địa điểm',
|
||||
'collections.duplicatedCount': 'Đã sao chép {count} địa điểm',
|
||||
'collections.placeCount': '{count} địa điểm',
|
||||
'collections.category': 'Danh mục',
|
||||
'collections.noCategory': 'Không có danh mục',
|
||||
'collections.editListTitle': 'Chỉnh sửa danh sách',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': '選擇',
|
||||
'collections.selectAll': '全選',
|
||||
'collections.deselectAll': '取消全選',
|
||||
'collections.moveToList': '移動到清單',
|
||||
'collections.duplicateToList': '複製到清單',
|
||||
'collections.moveToListTitle': '將 {count} 個移動到其他清單',
|
||||
'collections.duplicateToListTitle': '將 {count} 個複製到其他清單',
|
||||
'collections.noOtherLists': '尚無其他清單',
|
||||
'collections.movedCount': '已移動 {count} 個地點',
|
||||
'collections.duplicatedCount': '已複製 {count} 個地點',
|
||||
'collections.placeCount': '{count} 個地點',
|
||||
'collections.category': '類別',
|
||||
'collections.noCategory': '無類別',
|
||||
'collections.editListTitle': '編輯清單',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collection: TranslationStrings = {
|
||||
'collections.select': '选择',
|
||||
'collections.selectAll': '全选',
|
||||
'collections.deselectAll': '取消全选',
|
||||
'collections.moveToList': '移动到列表',
|
||||
'collections.duplicateToList': '复制到列表',
|
||||
'collections.moveToListTitle': '将 {count} 个移动到其他列表',
|
||||
'collections.duplicateToListTitle': '将 {count} 个复制到其他列表',
|
||||
'collections.noOtherLists': '暂无其他列表',
|
||||
'collections.movedCount': '已移动 {count} 个地点',
|
||||
'collections.duplicatedCount': '已复制 {count} 个地点',
|
||||
'collections.placeCount': '{count} 个地点',
|
||||
'collections.category': '类别',
|
||||
'collections.noCategory': '无类别',
|
||||
'collections.editListTitle': '编辑清单',
|
||||
|
||||
Reference in New Issue
Block a user