mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(packing): sort the packing list A to Z
A switch at the end of the list's tab row shows every list's items in name order, so checking whether something is already on the list no longer means reading all of it. The order follows the reader's language (accents beside their letter, "Shirt 2" before "Shirt 10") and the empty list stand-in stays at the bottom. Sorting only changes what is shown: the manual order stays stored and comes back with a second click. While sorted the drag grips step aside, the box keeps its column, and the choice is remembered per browser.
This commit is contained in:
@@ -236,6 +236,44 @@ describe('PackingListPanel', () => {
|
||||
expect(screen.getByText('No items match this filter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-093: the A to Z switch sorts each list by name and a second click restores the manual order', async () => {
|
||||
const user = userEvent.setup();
|
||||
const items = [
|
||||
buildPackingItem({ id: 1, name: 'Toothbrush', category: 'Bag' }),
|
||||
buildPackingItem({ id: 2, name: 'adapter', category: 'Bag' }),
|
||||
buildPackingItem({ id: 3, name: 'Shirt', category: 'Bag' }),
|
||||
];
|
||||
render(<PackingListPanel tripId={1} items={items} />);
|
||||
const order = () => ['Toothbrush', 'adapter', 'Shirt']
|
||||
.map(n => [n, screen.getByText(n)] as const)
|
||||
.sort(([, a], [, b]) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))
|
||||
.map(([n]) => n);
|
||||
const toggle = screen.getByRole('button', { name: 'A to Z' });
|
||||
|
||||
expect(order()).toEqual(['Toothbrush', 'adapter', 'Shirt']);
|
||||
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
||||
|
||||
await user.click(toggle);
|
||||
expect(order()).toEqual(['adapter', 'Shirt', 'Toothbrush']);
|
||||
expect(toggle).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(localStorage.getItem('trek:packing-sort')).toBe('name');
|
||||
|
||||
await user.click(toggle);
|
||||
expect(order()).toEqual(['Toothbrush', 'adapter', 'Shirt']);
|
||||
expect(localStorage.getItem('trek:packing-sort')).toBe('manual');
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-094: sorted A to Z the rows cannot be dragged, and the choice is remembered', () => {
|
||||
localStorage.setItem('trek:packing-sort', 'name');
|
||||
const items = [
|
||||
buildPackingItem({ id: 1, name: 'Toothbrush', category: 'Bag' }),
|
||||
buildPackingItem({ id: 2, name: 'adapter', category: 'Bag' }),
|
||||
];
|
||||
const { container } = render(<PackingListPanel tripId={1} items={items} />);
|
||||
expect(screen.getByRole('button', { name: 'A to Z' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(container.querySelector('[draggable="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-023: inline edit item name via pencil icon calls PUT', async () => {
|
||||
const user = userEvent.setup();
|
||||
const item = buildPackingItem({ id: 42, name: 'Sunscreen', category: 'Toiletries' });
|
||||
|
||||
@@ -35,6 +35,9 @@ interface KategorieGruppeProps {
|
||||
// order is global, so a within-category drag is mapped back onto the full list.
|
||||
allItems: PackingItem[]
|
||||
onReorder: (orderedIds: number[]) => void
|
||||
// Off while the list is shown A-Z: a drop there would move an item in an order
|
||||
// the reader cannot see.
|
||||
reorderable?: boolean
|
||||
// Three-tier sharing (#858) — threaded down to each item's share control.
|
||||
currentUserId?: number
|
||||
onSetSharing?: (id: number, visibility: 'common' | 'personal' | 'shared', recipientIds: number[]) => void
|
||||
@@ -43,7 +46,7 @@ interface KategorieGruppeProps {
|
||||
onLeave?: (id: number, userId: number) => void
|
||||
}
|
||||
|
||||
export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, onDeleteAll, onDeleteItem, onAddItem, assignees, tripMembers, onSetAssignees, bagTrackingEnabled, bags, onCreateBag, canEdit = true, allItems, onReorder, currentUserId, onSetSharing, onClone, onJoin, onLeave }: KategorieGruppeProps) {
|
||||
export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, onDeleteAll, onDeleteItem, onAddItem, assignees, tripMembers, onSetAssignees, bagTrackingEnabled, bags, onCreateBag, canEdit = true, allItems, onReorder, reorderable = true, currentUserId, onSetSharing, onClone, onJoin, onLeave }: KategorieGruppeProps) {
|
||||
const [offen, setOffen] = useState(true)
|
||||
const [dragId, setDragId] = useState<number | null>(null)
|
||||
const [overId, setOverId] = useState<number | null>(null)
|
||||
@@ -280,7 +283,7 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen
|
||||
<React.Fragment key={item.id}>
|
||||
<ArtikelZeile item={item} tripId={tripId} categories={allCategories} onCategoryChange={() => {}} onDelete={onDeleteItem} bagTrackingEnabled={bagTrackingEnabled} bags={bags} onCreateBag={onCreateBag} canEdit={canEdit} divider={index > 0}
|
||||
tripMembers={tripMembers} currentUserId={currentUserId} onSetSharing={onSetSharing} onClone={onClone} onJoin={onJoin} onLeave={onLeave}
|
||||
drag={canEdit ? {
|
||||
drag={canEdit && reorderable ? {
|
||||
isDragging: dragId === item.id,
|
||||
isOver: overId === item.id && dragId !== null && dragId !== item.id,
|
||||
onStart: (id) => { setDragId(id); setOverId(null) },
|
||||
|
||||
@@ -161,6 +161,8 @@ export function ArtikelZeile({ item, tripId, categories, onCategoryChange: _onCa
|
||||
<GripVertical size={13} />
|
||||
</div>
|
||||
)}
|
||||
{/* Sorted A-Z there is nothing to drag, but the box keeps its column. */}
|
||||
{!canDrag && canEdit && !isPlaceholder && <span aria-hidden style={{ width: 13, flexShrink: 0 }} />}
|
||||
{/* The phone's box: filled with the accent once packed, an outline until then. */}
|
||||
<button type="button" onClick={handleToggle} className="packing-check" aria-pressed={!!item.checked} style={{
|
||||
flexShrink: 0, cursor: 'pointer', padding: 0, width: 20, height: 20, borderRadius: 6,
|
||||
|
||||
@@ -7,7 +7,7 @@ export function PackingList(S: PackingState) {
|
||||
items, gruppiert, t, tripId, allCategories, handleRenameCategory, handleDeleteCategory, handleDeleteItem,
|
||||
handleAddItemToCategory, categoryAssignees, tripMembers, handleSetAssignees,
|
||||
bagTrackingEnabled, bags, handleCreateBagByName, canEdit, reorderPackingItems,
|
||||
currentUserId, handleSetSharing, handleCloneItem, handleJoinItem, handleLeaveItem,
|
||||
currentUserId, handleSetSharing, handleCloneItem, handleJoinItem, handleLeaveItem, sort,
|
||||
} = S
|
||||
return (
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '10px 0 16px' }}>
|
||||
@@ -39,6 +39,7 @@ export function PackingList(S: PackingState) {
|
||||
canEdit={canEdit}
|
||||
allItems={items}
|
||||
onReorder={(orderedIds) => reorderPackingItems(tripId, orderedIds)}
|
||||
reorderable={sort === 'manual'}
|
||||
currentUserId={currentUserId}
|
||||
onSetSharing={handleSetSharing}
|
||||
onClone={handleCloneItem}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Users, UserRound } from 'lucide-react'
|
||||
import { Users, UserRound, ArrowDownAZ } from 'lucide-react'
|
||||
import type { PackingState } from './usePackingListPanel'
|
||||
|
||||
/**
|
||||
* One tab row: the three-tier view switch (Gemeinsam / Meine Liste, #858) on the
|
||||
* left, and the all/open/done filter on the right. Each sits in its own tinted
|
||||
* track, the way the phone shows them, so the row reads as two questions
|
||||
* (whose list, which state) rather than five pills of equal weight.
|
||||
* left, then the all/open/done filter. Each sits in its own tinted track, the way
|
||||
* the phone shows them, so the row reads as two questions (whose list, which
|
||||
* state) rather than five pills of equal weight. The A-Z switch keeps to the far
|
||||
* end in a track of its own: it changes the order, not what is listed.
|
||||
*/
|
||||
export function PackingViewTabs(S: PackingState) {
|
||||
const { view, setView, filter, setFilter, t, items } = S
|
||||
const { view, setView, filter, setFilter, sort, setSort, t, items } = S
|
||||
const commonCount = items.filter(i => !i.is_private).length
|
||||
const personalCount = items.filter(i => !!i.is_private).length
|
||||
|
||||
@@ -55,6 +56,15 @@ export function PackingViewTabs(S: PackingState) {
|
||||
{filterPill('erledigt', t('packing.filterDone'))}
|
||||
</div>
|
||||
)}
|
||||
{items.length > 0 && (
|
||||
// A second click on the active switch goes back to the manual order.
|
||||
<div role="group" aria-label={t('packing.sortBy')} style={{ ...track, marginLeft: 'auto' }}>
|
||||
<button type="button" aria-pressed={sort === 'name'} title={t('packing.sortByName')}
|
||||
onClick={() => setSort(sort === 'name' ? 'manual' : 'name')} style={segment(sort === 'name')}>
|
||||
<ArrowDownAZ size={14} /><span className="hidden sm:inline">{t('packing.sortByName')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { katColor, itemWeight, bagFillPct, bagTotalWeight, countsTowardsMyLoad, isMarkdownList, parseCsvLine, parseImportLines, unassignedTotalWeight } from './packingListPanel.helpers'
|
||||
import { katColor, itemWeight, bagFillPct, bagTotalWeight, countsTowardsMyLoad, isMarkdownList, parseCsvLine, parseImportLines, sortItemsByName, unassignedTotalWeight } from './packingListPanel.helpers'
|
||||
import { KAT_COLORS } from './packingListPanel.constants'
|
||||
|
||||
describe('packingListPanel.helpers', () => {
|
||||
@@ -219,4 +219,26 @@ describe('packingListPanel.helpers', () => {
|
||||
expect(unassignedTotalWeight(undefined, [])).toBe(0)
|
||||
})
|
||||
})
|
||||
describe('sortItemsByName', () => {
|
||||
const names = (items: { name: string }[]) => items.map(i => i.name)
|
||||
|
||||
it('orders by name, ignoring case and accents the way the language does', () => {
|
||||
const items = [{ name: 'shirt' }, { name: 'Éponge' }, { name: 'adapter' }, { name: 'Zahnbürste' }, { name: 'Bag' }]
|
||||
expect(names(sortItemsByName(items, 'en'))).toEqual(['adapter', 'Bag', 'Éponge', 'shirt', 'Zahnbürste'])
|
||||
})
|
||||
|
||||
it('reads numbers as numbers', () => {
|
||||
expect(names(sortItemsByName([{ name: 'Shirt 10' }, { name: 'Shirt 2' }], 'en'))).toEqual(['Shirt 2', 'Shirt 10'])
|
||||
})
|
||||
|
||||
it('keeps the placeholder of an empty list at the bottom', () => {
|
||||
expect(names(sortItemsByName([{ name: '...' }, { name: 'Zip bag' }, { name: 'Adapter' }], 'en'))).toEqual(['Adapter', 'Zip bag', '...'])
|
||||
})
|
||||
|
||||
it('leaves the array it was given in its manual order', () => {
|
||||
const items = [{ name: 'b' }, { name: 'a' }]
|
||||
sortItemsByName(items, 'en')
|
||||
expect(names(items)).toEqual(['b', 'a'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KAT_COLORS } from './packingListPanel.constants'
|
||||
import { KAT_COLORS, PACKING_PLACEHOLDER_NAME } from './packingListPanel.constants'
|
||||
|
||||
// Stable color assignment: category name → index via simple hash
|
||||
export function katColor(kat: string, allCategories?: string[]): string {
|
||||
@@ -180,3 +180,19 @@ export const parseImportLines = (text: string): ParsedImportItem[] => {
|
||||
return { ...splitQuantity(parts[0]), category: undefined, weight_grams: undefined, bag: undefined, checked: false }
|
||||
}).filter(i => i.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* One list's items in name order, for the A-Z view. Compared the way the reader's
|
||||
* language sorts (accents next to their letter, "Shirt 2" before "Shirt 10"), and
|
||||
* the '...' stand-in of an empty list stays at the bottom where it always sits.
|
||||
* Returns a new array; the stored manual order is left alone.
|
||||
*/
|
||||
export function sortItemsByName<T extends { name: string }>(items: T[], locale?: string): T[] {
|
||||
const collator = new Intl.Collator(locale, { sensitivity: 'base', numeric: true })
|
||||
return [...items].sort((a, b) => {
|
||||
const pa = a.name === PACKING_PLACEHOLDER_NAME
|
||||
const pb = b.name === PACKING_PLACEHOLDER_NAME
|
||||
if (pa !== pb) return pa ? 1 : -1
|
||||
return collator.compare(a.name.trim(), b.name.trim())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@ import { useNetworkMode } from '../../hooks/useNetworkMode'
|
||||
import { useBagTotalsPing } from './useBagTotalsPing'
|
||||
import type { PackingItem, PackingBag } from '../../types'
|
||||
import { BAG_COLORS, PACKING_PLACEHOLDER_NAME } from './packingListPanel.constants'
|
||||
import { parseImportLines } from './packingListPanel.helpers'
|
||||
import { parseImportLines, sortItemsByName } from './packingListPanel.helpers'
|
||||
|
||||
const PACKING_SORT_KEY = 'trek:packing-sort'
|
||||
|
||||
export type PackingSort = 'manual' | 'name'
|
||||
|
||||
export interface TripMember {
|
||||
id: number
|
||||
@@ -50,6 +54,15 @@ export interface PackingListPanelProps {
|
||||
*/
|
||||
export function usePackingList({ tripId, items, openImportSignal = 0, addCategorySignal = 0, saveTemplateSignal = 0, inlineHeader = true, view: viewProp, onViewChange }: PackingListPanelProps) {
|
||||
const [filter, setFilter] = useState('alle') // 'alle' | 'offen' | 'erledigt'
|
||||
// A-Z only changes what is shown; the manual order stays stored underneath and
|
||||
// comes back when the switch is turned off. Remembered per browser.
|
||||
const [sort, setSortState] = useState<PackingSort>(() => {
|
||||
try { return localStorage.getItem(PACKING_SORT_KEY) === 'name' ? 'name' : 'manual' } catch { return 'manual' }
|
||||
})
|
||||
const setSort = (next: PackingSort) => {
|
||||
setSortState(next)
|
||||
try { localStorage.setItem(PACKING_SORT_KEY, next) } catch { /* storage unavailable: the choice lasts until reload */ }
|
||||
}
|
||||
// Three-tier sharing (#858): 'common' = the group pool (where existing items
|
||||
// live — non-breaking), 'personal' = my own list (private + shared-to-me).
|
||||
const [ownView, setOwnView] = useState<'common' | 'personal'>('common')
|
||||
@@ -65,7 +78,7 @@ export function usePackingList({ tripId, items, openImportSignal = 0, addCategor
|
||||
const isAdmin = useAuthStore((s) => s.user?.role === 'admin')
|
||||
const currentUserId = useAuthStore((s) => s.user?.id)
|
||||
const toast = useToast()
|
||||
const { t } = useTranslation()
|
||||
const { t, locale } = useTranslation()
|
||||
|
||||
// Trip members & category assignees
|
||||
const [tripMembers, setTripMembers] = useState<TripMember[]>([])
|
||||
@@ -120,8 +133,11 @@ export function usePackingList({ tripId, items, openImportSignal = 0, addCategor
|
||||
if (!groups[kat]) groups[kat] = []
|
||||
groups[kat].push(item)
|
||||
}
|
||||
if (sort === 'name') {
|
||||
for (const kat of Object.keys(groups)) groups[kat] = sortItemsByName(groups[kat], locale)
|
||||
}
|
||||
return groups
|
||||
}, [viewItems, filter, t])
|
||||
}, [viewItems, filter, sort, locale, t])
|
||||
|
||||
const abgehakt = viewItems.filter(i => i.checked).length
|
||||
const fortschritt = viewItems.length > 0 ? Math.round((abgehakt / viewItems.length) * 100) : 0
|
||||
@@ -399,7 +415,7 @@ export function usePackingList({ tripId, items, openImportSignal = 0, addCategor
|
||||
view, setView, currentUserId,
|
||||
handleSetSharing, handleCloneItem, handleJoinItem, handleLeaveItem,
|
||||
tripId, items, inlineHeader, t, canEdit, isAdmin, font, reorderPackingItems,
|
||||
filter, setFilter, addingCategory, setAddingCategory, newCatName, setNewCatName,
|
||||
filter, setFilter, sort, setSort, addingCategory, setAddingCategory, newCatName, setNewCatName,
|
||||
tripMembers, categoryAssignees, handleSetAssignees, allCategories, gruppiert, abgehakt, fortschritt,
|
||||
handleAddItemToCategory, handleAddNewCategory, handleRenameCategory, handleDeleteCategory, handleDeleteItem, handleClearChecked,
|
||||
bagTrackingEnabled, bags, unassignedWeightGrams, serverWeightsFresh, newBagName, setNewBagName, showAddBag, setShowAddBag, showBagModal, setShowBagModal,
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'الكل',
|
||||
'packing.filterOpen': 'مفتوح',
|
||||
'packing.filterDone': 'تم',
|
||||
'packing.sortBy': 'ترتيب حسب',
|
||||
'packing.sortByName': 'أ إلى ي',
|
||||
'packing.emptyTitle': 'قائمة التجهيز فارغة',
|
||||
'packing.emptyFiltered': 'لا توجد عناصر مطابقة لهذا الفلتر',
|
||||
'packing.menuRename': 'إعادة تسمية',
|
||||
|
||||
@@ -46,6 +46,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Hamısı',
|
||||
'packing.filterOpen': 'Hazır deyil',
|
||||
'packing.filterDone': 'Hazırdır',
|
||||
'packing.sortBy': 'Sıralama',
|
||||
'packing.sortByName': 'A-dan Z-yə',
|
||||
|
||||
'packing.emptyTitle': 'Baqaj siyahısı boşdur',
|
||||
'packing.emptyFiltered':
|
||||
|
||||
@@ -42,6 +42,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Todos',
|
||||
'packing.filterOpen': 'Abertos',
|
||||
'packing.filterDone': 'Prontos',
|
||||
'packing.sortBy': 'Ordenar por',
|
||||
'packing.sortByName': 'A a Z',
|
||||
'packing.emptyTitle': 'A lista de mala está vazia',
|
||||
'packing.emptyFiltered': 'Nenhum item corresponde ao filtro',
|
||||
'packing.menuRename': 'Renomear',
|
||||
|
||||
@@ -31,6 +31,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Tot',
|
||||
'packing.filterOpen': 'Pendents',
|
||||
'packing.filterDone': 'Fet',
|
||||
'packing.sortBy': 'Ordena per',
|
||||
'packing.sortByName': 'A a Z',
|
||||
'packing.emptyTitle': "La llista d'equipatge és buida",
|
||||
'packing.emptyFiltered': 'Cap element coincideix amb aquest filtre',
|
||||
'packing.menuRename': 'Canvia el nom',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Vše',
|
||||
'packing.filterOpen': 'K zabalení',
|
||||
'packing.filterDone': 'Hotovo',
|
||||
'packing.sortBy': 'Seřadit podle',
|
||||
'packing.sortByName': 'A až Z',
|
||||
'packing.emptyTitle': 'Seznam věcí je prázdný',
|
||||
'packing.emptyFiltered': 'Žádné položky neodpovídají filtru',
|
||||
'packing.menuRename': 'Přejmenovat',
|
||||
|
||||
@@ -41,6 +41,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Alle',
|
||||
'packing.filterOpen': 'Offen',
|
||||
'packing.filterDone': 'Erledigt',
|
||||
'packing.sortBy': 'Sortieren nach',
|
||||
'packing.sortByName': 'A bis Z',
|
||||
'packing.emptyTitle': 'Packliste ist leer',
|
||||
'packing.emptyFiltered': 'Keine Gegenstände in diesem Filter',
|
||||
'packing.menuRename': 'Umbenennen',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'All',
|
||||
'packing.filterOpen': 'Open',
|
||||
'packing.filterDone': 'Done',
|
||||
'packing.sortBy': 'Sort by',
|
||||
'packing.sortByName': 'A to Z',
|
||||
'packing.emptyTitle': 'Packing list is empty',
|
||||
'packing.emptyFiltered': 'No items match this filter',
|
||||
'packing.menuRename': 'Rename',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Todo',
|
||||
'packing.filterOpen': 'Pendientes',
|
||||
'packing.filterDone': 'Hecho',
|
||||
'packing.sortBy': 'Ordenar por',
|
||||
'packing.sortByName': 'A a Z',
|
||||
'packing.emptyTitle': 'La lista de equipaje está vacía',
|
||||
'packing.emptyFiltered': 'Ningún elemento coincide con este filtro',
|
||||
'packing.menuRename': 'Renombrar',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Kõik',
|
||||
'packing.filterOpen': 'Pakkimata',
|
||||
'packing.filterDone': 'Pakitud',
|
||||
'packing.sortBy': 'Sordi',
|
||||
'packing.sortByName': 'A kuni Ü',
|
||||
'packing.emptyTitle': 'Pakkimisnimekiri on tühi',
|
||||
'packing.emptyFiltered': 'Ükski ese ei vasta sellele filtrile',
|
||||
'packing.menuRename': 'Nimeta ümber',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Tous',
|
||||
'packing.filterOpen': 'À faire',
|
||||
'packing.filterDone': 'Fait',
|
||||
'packing.sortBy': 'Trier par',
|
||||
'packing.sortByName': 'A à Z',
|
||||
'packing.emptyTitle': 'La liste de bagages est vide',
|
||||
'packing.emptyFiltered': 'Aucun article ne correspond à ce filtre',
|
||||
'packing.menuRename': 'Renommer',
|
||||
|
||||
@@ -41,6 +41,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Όλα',
|
||||
'packing.filterOpen': 'Ανοιχτά',
|
||||
'packing.filterDone': 'Ολοκληρωμένα',
|
||||
'packing.sortBy': 'Ταξινόμηση κατά',
|
||||
'packing.sortByName': 'Α έως Ω',
|
||||
'packing.emptyTitle': 'Η λίστα αποσκευών είναι κενή',
|
||||
'packing.emptyFiltered': 'Κανένα αντικείμενο δεν ταιριάζει με αυτό το φίλτρο',
|
||||
'packing.menuRename': 'Μετονομασία',
|
||||
|
||||
@@ -41,6 +41,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Összes',
|
||||
'packing.filterOpen': 'Nyitott',
|
||||
'packing.filterDone': 'Kész',
|
||||
'packing.sortBy': 'Rendezés',
|
||||
'packing.sortByName': 'A-tól Z-ig',
|
||||
'packing.emptyTitle': 'A csomagolási lista üres',
|
||||
'packing.emptyFiltered': 'Nincs elem ebben a szűrőben',
|
||||
'packing.menuRename': 'Átnevezés',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Semua',
|
||||
'packing.filterOpen': 'Belum',
|
||||
'packing.filterDone': 'Selesai',
|
||||
'packing.sortBy': 'Urutkan',
|
||||
'packing.sortByName': 'A ke Z',
|
||||
'packing.emptyTitle': 'Daftar bawaan kosong',
|
||||
'packing.emptyFiltered': 'Tidak ada item yang cocok dengan filter ini',
|
||||
'packing.menuRename': 'Ganti Nama',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Tutti',
|
||||
'packing.filterOpen': 'Da fare',
|
||||
'packing.filterDone': 'Fatto',
|
||||
'packing.sortBy': 'Ordina per',
|
||||
'packing.sortByName': 'A-Z',
|
||||
'packing.emptyTitle': 'La lista valigia è vuota',
|
||||
'packing.emptyFiltered': 'Nessun elemento corrisponde a questo filtro',
|
||||
'packing.menuRename': 'Rinomina',
|
||||
|
||||
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'すべて',
|
||||
'packing.filterOpen': '未完了',
|
||||
'packing.filterDone': '完了',
|
||||
'packing.sortBy': '並べ替え',
|
||||
'packing.sortByName': '名前順',
|
||||
'packing.emptyTitle': '持ち物リストは空です',
|
||||
'packing.emptyFiltered': 'このフィルターに一致する項目はありません',
|
||||
'packing.menuRename': '名前を変更',
|
||||
|
||||
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': '전체',
|
||||
'packing.filterOpen': '미완료',
|
||||
'packing.filterDone': '완료',
|
||||
'packing.sortBy': '정렬',
|
||||
'packing.sortByName': '가나다순',
|
||||
'packing.emptyTitle': '짐 목록이 비어 있습니다',
|
||||
'packing.emptyFiltered': '이 필터와 일치하는 항목이 없습니다',
|
||||
'packing.menuRename': '이름 변경',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Alle',
|
||||
'packing.filterOpen': 'Openstaand',
|
||||
'packing.filterDone': 'Klaar',
|
||||
'packing.sortBy': 'Sorteren op',
|
||||
'packing.sortByName': 'A tot Z',
|
||||
'packing.emptyTitle': 'Inpaklijst is leeg',
|
||||
'packing.emptyFiltered': 'Geen items gevonden voor dit filter',
|
||||
'packing.menuRename': 'Hernoemen',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Wszystkie',
|
||||
'packing.filterOpen': 'Do spakowania',
|
||||
'packing.filterDone': 'Spakowane',
|
||||
'packing.sortBy': 'Sortuj według',
|
||||
'packing.sortByName': 'Od A do Z',
|
||||
'packing.emptyTitle': 'Lista pakowania jest pusta',
|
||||
'packing.emptyFiltered': 'Brak przedmiotów pasujących do filtra',
|
||||
'packing.menuRename': 'Zmień nazwę',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Все',
|
||||
'packing.filterOpen': 'Не собрано',
|
||||
'packing.filterDone': 'Собрано',
|
||||
'packing.sortBy': 'Сортировка',
|
||||
'packing.sortByName': 'От А до Я',
|
||||
'packing.emptyTitle': 'Список вещей пуст',
|
||||
'packing.emptyFiltered': 'Нет вещей, соответствующих фильтру',
|
||||
'packing.menuRename': 'Переименовать',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Všetko',
|
||||
'packing.filterOpen': 'Na zabalenie',
|
||||
'packing.filterDone': 'Hotovo',
|
||||
'packing.sortBy': 'Zoradiť podľa',
|
||||
'packing.sortByName': 'A až Z',
|
||||
'packing.emptyTitle': 'Zoznam vecí je prázdny',
|
||||
'packing.emptyFiltered': 'Žiadne položky nezodpovedajú tomuto filtru',
|
||||
'packing.menuRename': 'Premenovať',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Alla',
|
||||
'packing.filterOpen': 'Öppna',
|
||||
'packing.filterDone': 'Klar',
|
||||
'packing.sortBy': 'Sortera efter',
|
||||
'packing.sortByName': 'A till Ö',
|
||||
'packing.emptyTitle': 'Packlistan är tom',
|
||||
'packing.emptyFiltered': 'Inga föremål matchar detta filter',
|
||||
'packing.menuRename': 'Döp om',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': "ทั้งหมด",
|
||||
'packing.filterOpen': "ยังไม่ได้จัด",
|
||||
'packing.filterDone': "เสร็จแล้ว",
|
||||
'packing.sortBy': 'เรียงตาม',
|
||||
'packing.sortByName': 'ก ถึง ฮ',
|
||||
'packing.emptyTitle': "รายการจัดกระเป๋าว่างเปล่า",
|
||||
'packing.emptyFiltered': "ไม่มีรายการที่ตรงกับตัวกรองนี้",
|
||||
'packing.menuRename': "เปลี่ยนชื่อ",
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Tüm',
|
||||
'packing.filterOpen': 'Açık',
|
||||
'packing.filterDone': 'Tamamlamak',
|
||||
'packing.sortBy': 'Sırala',
|
||||
'packing.sortByName': "A'dan Z'ye",
|
||||
'packing.emptyTitle': 'Paket listesi boş',
|
||||
'packing.emptyFiltered': 'Bu filtreye uyan öğe yok',
|
||||
'packing.menuRename': 'Yeniden adlandır',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Усі',
|
||||
'packing.filterOpen': 'Не зібрано',
|
||||
'packing.filterDone': 'Зібрано',
|
||||
'packing.sortBy': 'Сортування',
|
||||
'packing.sortByName': 'Від А до Я',
|
||||
'packing.emptyTitle': 'Список речей порожній',
|
||||
'packing.emptyFiltered': 'Немає речей, що відповідають фільтру',
|
||||
'packing.menuRename': 'Перейменувати',
|
||||
|
||||
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': 'Tất cả',
|
||||
'packing.filterOpen': 'Mở',
|
||||
'packing.filterDone': 'Xong',
|
||||
'packing.sortBy': 'Sắp xếp theo',
|
||||
'packing.sortByName': 'A đến Z',
|
||||
'packing.emptyTitle': 'Danh sách đóng gói trống',
|
||||
'packing.emptyFiltered': 'Không có mục nào phù hợp với bộ lọc này',
|
||||
'packing.menuRename': 'Đổi tên',
|
||||
|
||||
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': '全部',
|
||||
'packing.filterOpen': '未完成',
|
||||
'packing.filterDone': '已完成',
|
||||
'packing.sortBy': '排序方式',
|
||||
'packing.sortByName': '依名稱',
|
||||
'packing.emptyTitle': '行李清單為空',
|
||||
'packing.emptyFiltered': '沒有匹配的物品',
|
||||
'packing.menuRename': '重新命名',
|
||||
|
||||
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
|
||||
'packing.filterAll': '全部',
|
||||
'packing.filterOpen': '未完成',
|
||||
'packing.filterDone': '已完成',
|
||||
'packing.sortBy': '排序方式',
|
||||
'packing.sortByName': '按名称',
|
||||
'packing.emptyTitle': '行李清单为空',
|
||||
'packing.emptyFiltered': '没有匹配的物品',
|
||||
'packing.menuRename': '重命名',
|
||||
|
||||
Reference in New Issue
Block a user