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:
Maurice
2026-09-29 20:25:25 +02:00
parent 6f61b66c5c
commit 272df55cd4
35 changed files with 176 additions and 14 deletions
@@ -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,
+2
View File
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
'packing.filterAll': 'الكل',
'packing.filterOpen': 'مفتوح',
'packing.filterDone': 'تم',
'packing.sortBy': 'ترتيب حسب',
'packing.sortByName': 'أ إلى ي',
'packing.emptyTitle': 'قائمة التجهيز فارغة',
'packing.emptyFiltered': 'لا توجد عناصر مطابقة لهذا الفلتر',
'packing.menuRename': 'إعادة تسمية',
+2
View File
@@ -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':
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -41,6 +41,8 @@ const packing: TranslationStrings = {
'packing.filterAll': 'Όλα',
'packing.filterOpen': 'Ανοιχτά',
'packing.filterDone': 'Ολοκληρωμένα',
'packing.sortBy': 'Ταξινόμηση κατά',
'packing.sortByName': 'Α έως Ω',
'packing.emptyTitle': 'Η λίστα αποσκευών είναι κενή',
'packing.emptyFiltered': 'Κανένα αντικείμενο δεν ταιριάζει με αυτό το φίλτρο',
'packing.menuRename': 'Μετονομασία',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
'packing.filterAll': 'すべて',
'packing.filterOpen': '未完了',
'packing.filterDone': '完了',
'packing.sortBy': '並べ替え',
'packing.sortByName': '名前順',
'packing.emptyTitle': '持ち物リストは空です',
'packing.emptyFiltered': 'このフィルターに一致する項目はありません',
'packing.menuRename': '名前を変更',
+2
View File
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
'packing.filterAll': '전체',
'packing.filterOpen': '미완료',
'packing.filterDone': '완료',
'packing.sortBy': '정렬',
'packing.sortByName': '가나다순',
'packing.emptyTitle': '짐 목록이 비어 있습니다',
'packing.emptyFiltered': '이 필터와 일치하는 항목이 없습니다',
'packing.menuRename': '이름 변경',
+2
View File
@@ -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',
+2
View File
@@ -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ę',
+2
View File
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
'packing.filterAll': 'Все',
'packing.filterOpen': 'Не собрано',
'packing.filterDone': 'Собрано',
'packing.sortBy': 'Сортировка',
'packing.sortByName': 'От А до Я',
'packing.emptyTitle': 'Список вещей пуст',
'packing.emptyFiltered': 'Нет вещей, соответствующих фильтру',
'packing.menuRename': 'Переименовать',
+2
View File
@@ -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ť',
+2
View File
@@ -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',
+2
View File
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
'packing.filterAll': "ทั้งหมด",
'packing.filterOpen': "ยังไม่ได้จัด",
'packing.filterDone': "เสร็จแล้ว",
'packing.sortBy': 'เรียงตาม',
'packing.sortByName': 'ก ถึง ฮ',
'packing.emptyTitle': "รายการจัดกระเป๋าว่างเปล่า",
'packing.emptyFiltered': "ไม่มีรายการที่ตรงกับตัวกรองนี้",
'packing.menuRename': "เปลี่ยนชื่อ",
+2
View File
@@ -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',
+2
View File
@@ -40,6 +40,8 @@ const packing: TranslationStrings = {
'packing.filterAll': 'Усі',
'packing.filterOpen': 'Не зібрано',
'packing.filterDone': 'Зібрано',
'packing.sortBy': 'Сортування',
'packing.sortByName': 'Від А до Я',
'packing.emptyTitle': 'Список речей порожній',
'packing.emptyFiltered': 'Немає речей, що відповідають фільтру',
'packing.menuRename': 'Перейменувати',
+2
View File
@@ -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',
+2
View File
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
'packing.filterAll': '全部',
'packing.filterOpen': '未完成',
'packing.filterDone': '已完成',
'packing.sortBy': '排序方式',
'packing.sortByName': '依名稱',
'packing.emptyTitle': '行李清單為空',
'packing.emptyFiltered': '沒有匹配的物品',
'packing.menuRename': '重新命名',
+2
View File
@@ -39,6 +39,8 @@ const packing: TranslationStrings = {
'packing.filterAll': '全部',
'packing.filterOpen': '未完成',
'packing.filterDone': '已完成',
'packing.sortBy': '排序方式',
'packing.sortByName': '按名称',
'packing.emptyTitle': '行李清单为空',
'packing.emptyFiltered': '没有匹配的物品',
'packing.menuRename': '重命名',