feat(collections): fusion sharing UI + dashboard widget + per-user toggle (#1081)

- ShareCollectionModal: the owner manages a list's members and invites users
  (available-users picker → invite, cancel pending); a member can leave a shared
  list. The incoming accept/decline surface stays in the lists rail. A Share
  button is added to the collections header for owners (and members, to reach
  Leave).
- CollectionsWidget: a dashboard glass card after the currency widget showing the
  saved count and the most recent saved places, double-gated by the admin addon
  and a new per-user appearance flag.
- Appearance: a 'collections' dashboard-widget flag (desktop + mobile defaults)
  wired into the appearance settings, surviving normalize.
- Sharing + settings strings across all 22 locales (parity strict passes).
This commit is contained in:
Maurice
2026-06-30 22:59:04 +02:00
committed by Maurice
parent 783332cc0e
commit 5f9da5b72f
53 changed files with 640 additions and 5 deletions
@@ -0,0 +1,266 @@
import React, { useEffect, useMemo, useState } from 'react'
import { UserPlus, Loader2, Clock, Check, Crown, LogOut } from 'lucide-react'
import type { CollectionMember } from '@trek/shared'
import Modal from '../shared/Modal'
import CustomSelect from '../shared/CustomSelect'
import { useToast } from '../shared/Toast'
import { useCollectionStore } from '../../store/collectionStore'
import { useAuthStore } from '../../store/authStore'
import { collectionsApi } from '../../api/collections'
import { getApiErrorMessage } from '../../utils/apiError'
import type { TranslationFn } from '../../types'
interface ShareCollectionModalProps {
isOpen: boolean
onClose: () => void
collectionId: number
collectionName: string
isOwner: boolean
members: CollectionMember[]
/** Called after the current (member) user successfully leaves the list. */
onAfterLeave: () => void
t: TranslationFn
}
function MemberAvatar({ member }: { member: CollectionMember }): React.ReactElement {
const initial = (member.username || '?').charAt(0).toUpperCase()
return (
<span className="w-8 h-8 rounded-full shrink-0 overflow-hidden flex items-center justify-center bg-surface-secondary text-content-secondary text-[12px] font-semibold">
{member.avatar ? (
<img src={`/uploads/avatars/${member.avatar}`} alt="" className="w-full h-full object-cover" />
) : (
initial
)}
</span>
)
}
/**
* Fusion-share surface for a single list (blueprint 4.4 / 4.8). The OWNER sees the
* member roster with accepted/pending status, can invite a user from
* GET /:id/available-users and cancel a pending invite. A non-owner MEMBER sees the
* roster read-only plus a "Leave shared list" action (the server blocks the owner
* from leaving). Incoming invites are accepted/declined from the lists rail, not here.
*/
export default function ShareCollectionModal({
isOpen,
onClose,
collectionId,
collectionName,
isOwner,
members,
onAfterLeave,
t,
}: ShareCollectionModalProps): React.ReactElement | null {
const toast = useToast()
const currentUserId = useAuthStore(s => s.user?.id)
const invite = useCollectionStore(s => s.invite)
const cancelInvite = useCollectionStore(s => s.cancelInvite)
const leave = useCollectionStore(s => s.leave)
const [availableUsers, setAvailableUsers] = useState<{ id: number; username: string }[]>([])
const [selectedUserId, setSelectedUserId] = useState<number | ''>('')
const [inviting, setInviting] = useState(false)
const [cancellingId, setCancellingId] = useState<number | null>(null)
const [confirmLeave, setConfirmLeave] = useState(false)
const [leaving, setLeaving] = useState(false)
// Load the invitable users whenever an owner opens the modal.
useEffect(() => {
if (!isOpen || !isOwner) return
let cancelled = false
collectionsApi.availableUsers(collectionId)
.then(data => { if (!cancelled) setAvailableUsers(data.users) })
.catch(() => { if (!cancelled) setAvailableUsers([]) })
return () => { cancelled = true }
}, [isOpen, isOwner, collectionId, members.length])
// Reset transient state on close.
useEffect(() => {
if (!isOpen) {
setSelectedUserId('')
setConfirmLeave(false)
}
}, [isOpen])
const sortedMembers = useMemo(() => {
// Owner first, then accepted, then pending — alphabetised within each band.
const rank = (m: CollectionMember) => (m.is_owner ? 0 : m.status === 'accepted' ? 1 : 2)
return [...members].sort((a, b) => rank(a) - rank(b) || a.username.localeCompare(b.username))
}, [members])
if (!isOpen) return null
const handleInvite = async () => {
if (selectedUserId === '' || inviting) return
setInviting(true)
try {
await invite(collectionId, Number(selectedUserId))
toast.success(t('collections.invite.sent'))
setSelectedUserId('')
} catch (err) {
toast.error(getApiErrorMessage(err, t('collections.invite.error')))
} finally {
setInviting(false)
}
}
const handleCancel = async (userId: number) => {
if (cancellingId != null) return
setCancellingId(userId)
try {
await cancelInvite(collectionId, userId)
} catch (err) {
toast.error(getApiErrorMessage(err, t('common.error')))
} finally {
setCancellingId(null)
}
}
const handleLeave = async () => {
setLeaving(true)
try {
await leave(collectionId)
toast.success(t('collections.share.left'))
onAfterLeave()
} catch (err) {
toast.error(getApiErrorMessage(err, t('common.error')))
} finally {
setLeaving(false)
}
}
return (
<Modal
isOpen
onClose={onClose}
title={t('collections.share.titleNamed', { name: collectionName })}
size="sm"
>
<div className="flex flex-col gap-5">
{/* Member roster */}
<div>
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-content-faint mb-2">
{t('collections.share.members')}
</h3>
<div className="flex flex-col gap-1">
{sortedMembers.map(member => {
const isSelf = member.user_id === currentUserId
const pending = member.status === 'pending'
return (
<div
key={member.user_id}
className={`flex items-center gap-2.5 px-2 py-1.5 rounded-lg ${pending ? 'opacity-80' : ''}`}
>
<MemberAvatar member={member} />
<div className="flex-1 min-w-0">
<p className="text-[13px] font-medium text-content truncate flex items-center gap-1.5">
{member.username}
{isSelf && <span className="text-content-faint font-normal">({t('collections.share.you')})</span>}
</p>
{member.email && !pending && (
<p className="text-[11px] text-content-faint truncate">{member.email}</p>
)}
</div>
{member.is_owner ? (
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-content-faint shrink-0">
<Crown size={12} /> {t('collections.share.owner')}
</span>
) : pending ? (
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-amber-500 shrink-0">
<Clock size={12} /> {t('collections.share.pending')}
</span>
) : (
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-emerald-500 shrink-0">
<Check size={12} /> {t('collections.share.accepted')}
</span>
)}
{isOwner && pending && (
<button
type="button"
onClick={() => handleCancel(member.user_id)}
disabled={cancellingId === member.user_id}
className="shrink-0 text-[11px] font-medium px-2 py-1 rounded-md text-content-faint hover:text-danger hover:bg-danger-soft transition-colors disabled:opacity-50"
>
{cancellingId === member.user_id ? <Loader2 size={12} className="animate-spin" /> : t('collections.share.cancel')}
</button>
)}
</div>
)
})}
</div>
</div>
{isOwner ? (
/* Owner: invite UI */
<div className="pt-1 border-t border-edge-secondary">
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-content-faint mt-4 mb-2">
{t('collections.share.invite')}
</h3>
<p className="text-[12px] text-content-muted mb-3">{t('collections.share.inviteHint')}</p>
{availableUsers.length === 0 ? (
<p className="text-[12px] text-content-faint text-center py-3">{t('collections.share.noUsers')}</p>
) : (
<div className="flex items-stretch gap-2">
<div className="flex-1 min-w-0">
<CustomSelect
value={selectedUserId}
onChange={v => setSelectedUserId(v === '' ? '' : Number(v))}
options={availableUsers.map(u => ({ value: u.id, label: u.username }))}
placeholder={t('collections.share.inviteUser')}
searchable
/>
</div>
<button
type="button"
onClick={handleInvite}
disabled={selectedUserId === '' || inviting}
className="shrink-0 inline-flex items-center gap-1.5 px-3 py-2 rounded-lg bg-accent text-accent-text text-[13px] font-semibold hover:bg-accent-hover transition-colors disabled:opacity-50"
>
{inviting ? <Loader2 size={14} className="animate-spin" /> : <UserPlus size={14} />}
<span className="hidden sm:inline">{t('collections.share.sendInvite')}</span>
</button>
</div>
)}
</div>
) : (
/* Member: read-only roster + leave */
<div className="pt-1 border-t border-edge-secondary">
<p className="text-[12px] text-content-muted mt-4 mb-3">{t('collections.share.memberHint')}</p>
{confirmLeave ? (
<div className="flex flex-col gap-2.5">
<p className="text-[13px] text-content-secondary">{t('collections.share.leaveConfirm')}</p>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setConfirmLeave(false)}
className="px-3 py-1.5 rounded-lg border border-edge text-content-secondary text-[13px] hover:bg-surface-hover transition-colors"
>
{t('common.cancel')}
</button>
<button
type="button"
onClick={handleLeave}
disabled={leaving}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-danger text-white text-[13px] font-semibold hover:opacity-90 transition-opacity disabled:opacity-50"
>
{leaving ? <Loader2 size={14} className="animate-spin" /> : <LogOut size={14} />}
{t('collections.share.leave')}
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setConfirmLeave(true)}
className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg border border-edge text-danger text-[13px] font-medium hover:bg-danger-soft transition-colors"
>
<LogOut size={14} /> {t('collections.share.leave')}
</button>
)}
</div>
)}
</div>
</Modal>
)
}
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react'
import { Bookmark, ArrowRight } from 'lucide-react'
import { useTranslation } from '../../i18n'
import PlaceAvatar from '../shared/PlaceAvatar'
import { collectionsApi } from '../../api/collections'
import type { CollectionPlace } from '@trek/shared'
/**
* Dashboard sidebar widget — a glassy `.tool` card (cloned from the dashboard's
* UpcomingTool) that surfaces the user's saved-places library: a total count
* plus the most recently saved places. Every row and the header "→" jump to
* /collections.
*
* Self-contained on purpose: it fetches its own data on mount, and it is only
* ever mounted when BOTH gates are satisfied (admin addon enabled AND the
* per-user widget flag on — see DashboardPage `showCollections`). That keeps the
* data fetch strictly "only when shown" and means the dashboard never hits
* /api/addons/collections while the addon is disabled.
*/
export default function CollectionsWidget({ onOpen }: { onOpen: () => void }): React.ReactElement {
const { t } = useTranslation()
const [places, setPlaces] = useState<CollectionPlace[]>([])
const [count, setCount] = useState(0)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
;(async () => {
try {
const data = await collectionsApi.list()
if (cancelled) return
// Union every list the user owns or co-owns, deduped, newest first —
// mirrors the store's "All saved" pseudo-list so the widget shows the
// same recent saves the page does.
const results = await Promise.all(data.collections.map(c => collectionsApi.get(c.id).catch(() => null)))
if (cancelled) return
const seen = new Set<number>()
const merged: CollectionPlace[] = []
for (const res of results) {
if (!res) continue
for (const p of res.places) {
if (seen.has(p.id)) continue
seen.add(p.id)
merged.push(p)
}
}
merged.sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''))
setCount(merged.length)
setPlaces(merged.slice(0, 4))
} catch {
if (!cancelled) { setPlaces([]); setCount(0) }
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => { cancelled = true }
}, [])
return (
<div className="tool">
<div className="tool-head">
<div className="tool-title"><Bookmark size={14} /> {t('collections.widget.title')}</div>
<button className="tool-action" aria-label={t('collections.widget.title')} onClick={onOpen}>
<ArrowRight size={14} />
</button>
</div>
{loading ? null : places.length === 0 ? (
<div className="col-empty">{t('collections.widget.empty')}</div>
) : (
<>
<div className="col-count">{t('collections.widget.savedCount', { count })}</div>
<div className="col-list">
{places.map(p => (
<button key={p.id} className="col-item" onClick={onOpen}>
<PlaceAvatar
place={p}
size={36}
category={p.category ? { color: p.category.color ?? undefined, icon: p.category.icon ?? undefined } : null}
/>
<div className="col-info">
<div className="t">{p.name}</div>
<div className="s">{p.category?.name || p.address || ''}</div>
</div>
</button>
))}
</div>
</>
)}
</div>
)
}
@@ -41,6 +41,7 @@ type MobileWidgetKey = keyof AppearanceConfig['dashboard']['mobile']
const WIDGET_LABELS: Record<string, string> = {
sidebar: 'Right sidebar',
currency: 'Currency',
collections: 'Collections',
timezones: 'Timezones',
upcomingReservations: 'Upcoming reservations',
atlas: 'Atlas / countries',
@@ -53,11 +54,11 @@ const WIDGET_LABELS: Record<string, string> = {
// widgets only matter while the sidebar is shown.
const DESKTOP_GROUPS: { id: string; fallback: string; master?: DesktopWidgetKey; keys: DesktopWidgetKey[] }[] = [
{ id: 'belowHero', fallback: 'Below the hero', keys: ['atlas', 'tripsTotal', 'daysTraveled', 'distanceFlown'] },
{ id: 'rightSidebar', fallback: 'Right sidebar', master: 'sidebar', keys: ['currency', 'timezones', 'upcomingReservations'] },
{ id: 'rightSidebar', fallback: 'Right sidebar', master: 'sidebar', keys: ['currency', 'collections', 'timezones', 'upcomingReservations'] },
]
const MOBILE_GROUPS: { id: string; fallback: string; keys: MobileWidgetKey[] }[] = [
{ id: 'belowHero', fallback: 'Below the hero', keys: ['tripsTotal', 'daysTraveled'] },
{ id: 'bottomOfPage', fallback: 'Bottom of page', keys: ['currency', 'timezones', 'upcomingReservations'] },
{ id: 'bottomOfPage', fallback: 'Bottom of page', keys: ['currency', 'collections', 'timezones', 'upcomingReservations'] },
]
// shared segmented-button style (matches DisplaySettingsTab)
+31 -1
View File
@@ -1,5 +1,5 @@
import React from 'react'
import { LayoutGrid, List as ListIcon, Map as MapIcon, Search, Bookmark, Plus, CheckSquare, X, Trash2, Copy } from 'lucide-react'
import { LayoutGrid, List as ListIcon, Map as MapIcon, Search, Bookmark, Plus, CheckSquare, X, Trash2, Copy, Share2, Users } from 'lucide-react'
import Navbar from '../components/Layout/Navbar'
import Modal from '../components/shared/Modal'
import ListsRail from '../components/Collections/ListsRail'
@@ -7,6 +7,7 @@ import CollectionGrid from '../components/Collections/CollectionGrid'
import CollectionList from '../components/Collections/CollectionList'
import CollectionMap from '../components/Collections/CollectionMap'
import CopyToTripModal from '../components/Collections/CopyToTripModal'
import ShareCollectionModal from '../components/Collections/ShareCollectionModal'
import PlaceInspector from '../components/Planner/PlaceInspector'
import type { TranslationFn } from '../types'
import type { CollectionView, StatusFilter } from '../store/collectionStore'
@@ -199,6 +200,21 @@ export default function CollectionsPage(): React.ReactElement {
</div>
<div className="flex items-center gap-2">
<ViewSwitch view={c.view} onChange={c.setView} t={t} />
{c.canShare && (
<button
type="button"
onClick={() => c.setShowShare(true)}
className="relative inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[12px] font-medium border bg-surface-card text-content-secondary border-edge hover:bg-surface-hover transition-colors"
>
{c.isOwner ? <Share2 size={14} /> : <Users size={14} />}
<span className="hidden sm:inline">{c.isOwner ? t('collections.share.button') : t('collections.shared')}</span>
{c.shareMemberCount > 0 && (
<span className="inline-flex items-center justify-center min-w-[16px] h-4 px-1 rounded-full bg-accent text-accent-text text-[10px] font-semibold tabular-nums">
{c.shareMemberCount}
</span>
)}
</button>
)}
{showSelect && (
<button
type="button"
@@ -314,6 +330,20 @@ export default function CollectionsPage(): React.ReactElement {
t={t}
/>
{/* Share / fusion */}
{c.canShare && typeof c.activeId === 'number' && c.activeCollection && (
<ShareCollectionModal
isOpen={c.showShare}
onClose={() => c.setShowShare(false)}
collectionId={c.activeId}
collectionName={c.activeCollection.name}
isOwner={c.isOwner}
members={c.members}
onAfterLeave={c.handleAfterLeave}
t={t}
/>
)}
{/* New list modal */}
<Modal
isOpen={c.showNewList}
+7 -1
View File
@@ -19,9 +19,11 @@ import {
LayoutGrid, List, Ticket, X, CalendarPlus,
} from 'lucide-react'
import { IcsSubscribeModal } from '../components/Planner/IcsSubscribeModal'
import CollectionsWidget from '../components/Dashboard/CollectionsWidget'
import { formatTime, splitReservationDateTime } from '../utils/formatters'
import { convertDistance, getDistanceUnitLabel } from '../utils/units'
import { useSettingsStore } from '../store/settingsStore'
import { useAddonStore } from '../store/addonStore'
import { normalizeAppearance } from '@trek/shared'
import '../styles/dashboard.css'
@@ -119,8 +121,11 @@ export default function DashboardPage(): React.ReactElement {
const showCurrency = sideWidgets.currency
const showTimezones = sideWidgets.timezones
const showUpcoming = sideWidgets.upcomingReservations
// Collections is double-gated: the admin addon AND the per-user widget flag.
const isAddonEnabled = useAddonStore(s => s.isEnabled)
const showCollections = isAddonEnabled('collections') && sideWidgets.collections
// Desktop has a master toggle for the whole right column; off → centered layout.
const sidebarVisible = (isMobile || dashCfg.desktop.sidebar) && (showCurrency || showTimezones || showUpcoming)
const sidebarVisible = (isMobile || dashCfg.desktop.sidebar) && (showCurrency || showCollections || showTimezones || showUpcoming)
return (
<>
@@ -226,6 +231,7 @@ export default function DashboardPage(): React.ReactElement {
{sidebarVisible && (
<aside className="page-sidebar">
{showCurrency && <CurrencyTool />}
{showCollections && <CollectionsWidget onOpen={() => navigate('/collections')} />}
{showTimezones && <TimezoneTool locale={locale} />}
{showUpcoming && <UpcomingTool items={upcoming} locale={locale} onOpen={(tripId) => navigate(`/trips/${tripId}`)} />}
</aside>
@@ -48,6 +48,7 @@ export function useCollections() {
const [confirmDeleteList, setConfirmDeleteList] = useState<number | null>(null)
const [mobileRailOpen, setMobileRailOpen] = useState(false)
const [creating, setCreating] = useState(false)
const [showShare, setShowShare] = useState(false)
// The place ids the Copy-to-trip modal is open for (null = closed). Single
// place from the detail panel, or the select-mode set for a bulk copy.
const [copyIds, setCopyIds] = useState<number[] | null>(null)
@@ -97,6 +98,14 @@ export function useCollections() {
const isAllSaved = activeId === ALL_SAVED
const isOwner = activeCollection?.is_owner ?? false
// Sharing is offered on any real (non "All saved") list the user can see. The
// badge counts everyone but the owner — accepted collaborators + pending invites.
const canShare = typeof activeId === 'number' && activeCollection != null
const shareMemberCount = useMemo(() => members.filter(m => !m.is_owner).length, [members])
// Close the share modal if the active list changes out from under it.
useEffect(() => { setShowShare(false) }, [activeId])
const ownedLists = useMemo(() => collections.filter(c => c.is_owner !== false), [collections])
const sharedLists = useMemo(() => collections.filter(c => c.is_owner === false), [collections])
@@ -210,6 +219,13 @@ export function useCollections() {
}
}, [declineInvite, toast, t])
// Member left the active shared list — the store already cleared it; bounce to
// the rail and close the share modal. Errors are surfaced inside the modal.
const handleAfterLeave = useCallback(() => {
setShowShare(false)
navigate('/collections')
}, [navigate])
// ── Detail panel (re-pointed PlaceInspector in collection mode) ──────
const selectedPlace = useMemo(
() => places.find(p => p.id === selectedPlaceId) ?? null,
@@ -263,6 +279,7 @@ export function useCollections() {
t, language, dark, navigate,
// store data
collections, ownedLists, sharedLists, activeCollection, isAllSaved, isOwner,
canShare, shareMemberCount,
activeId, places, visiblePlaces, members, incomingInvites, counts,
view, statusFilter, search, selectedPlaceId, selectMode, selectedIds,
loading, placesLoading,
@@ -273,6 +290,7 @@ export function useCollections() {
editingListId, editingName, setEditingName,
confirmDeleteList, setConfirmDeleteList,
mobileRailOpen, setMobileRailOpen,
showShare, setShowShare, handleAfterLeave,
// detail panel + copy-to-trip
selectedPlace, detailPlace, detailCategories, handleCloseDetail,
handleDetailStatus, handleDetailRemove,
+10
View File
@@ -541,6 +541,16 @@
.trek-dash .upc-type.other { background: var(--bg-2); color: var(--ink-2); }
.trek-dash .upc-type svg { width: 16px; height: 16px; }
/* ----------------- collections widget ----------------- */
.trek-dash .col-count { font-size: 12px; color: var(--ink-3); font-weight: 500; margin-bottom: 12px; }
.trek-dash .col-empty { font-size: 13px; color: var(--ink-3); }
.trek-dash .col-list { display: flex; flex-direction: column; gap: 4px; }
.trek-dash .col-item { display: grid; grid-template-columns: 36px 1fr; gap: 12px; padding: 8px; border-radius: 12px; align-items: center; transition: background .12s; cursor: pointer; width: 100%; text-align: left; }
.trek-dash .col-item:hover { background: var(--surface-2); }
.trek-dash .col-info { min-width: 0; }
.trek-dash .col-info .t { font-size: 14px; font-weight: 500; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trek-dash .col-info .s { font-size: 12px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ----------------- responsive ----------------- */
@media (max-width: 1280px) {
.trek-dash .page { padding-left: 32px; padding-right: 32px; grid-template-columns: 1fr; }
@@ -63,6 +63,17 @@ describe('normalizeAppearance', () => {
expect(out.dashboard.mobile.tripsTotal).toBe(true);
});
it('round-trips the collections widget flag on desktop + mobile and defaults a legacy blob to true', () => {
// explicit off on both devices survives normalization
const off = normalizeAppearance({ dashboard: { desktop: { collections: false }, mobile: { collections: false } } });
expect(off.dashboard.desktop.collections).toBe(false);
expect(off.dashboard.mobile.collections).toBe(false);
// a pre-collections blob (flag absent) defaults the widget on
const legacy = normalizeAppearance({ dashboard: { desktop: { sidebar: true }, mobile: { tripsTotal: true } } });
expect(legacy.dashboard.desktop.collections).toBe(true);
expect(legacy.dashboard.mobile.collections).toBe(true);
});
it('ignores bogus dashboard values without throwing', () => {
const out = normalizeAppearance({ dashboard: 'nope' });
expect(out.dashboard).toEqual(DEFAULT_APPEARANCE.dashboard);
@@ -66,6 +66,7 @@ const widgetFlag = z.boolean().catch(true).default(true);
const DEFAULT_DASHBOARD_DESKTOP = {
sidebar: true, // master toggle for the whole right sidebar (off → center)
currency: true,
collections: true, // saved-places library widget (still gated by the admin addon)
timezones: true,
upcomingReservations: true,
atlas: true, // the "countries visited" passport tile
@@ -77,6 +78,7 @@ const DEFAULT_DASHBOARD_MOBILE = {
tripsTotal: true,
daysTraveled: true,
currency: true,
collections: true,
timezones: true,
upcomingReservations: true,
};
@@ -91,6 +93,7 @@ const dashboardWidgetsSchema = z
.object({
sidebar: widgetFlag,
currency: widgetFlag,
collections: widgetFlag,
timezones: widgetFlag,
upcomingReservations: widgetFlag,
atlas: widgetFlag,
@@ -105,6 +108,7 @@ const dashboardWidgetsSchema = z
tripsTotal: widgetFlag,
daysTraveled: widgetFlag,
currency: widgetFlag,
collections: widgetFlag,
timezones: widgetFlag,
upcomingReservations: widgetFlag,
})
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'استخدام',
'collections.share.title': 'مشاركة القائمة',
'collections.share.titleNamed': 'مشاركة «{name}»',
'collections.share.button': 'مشاركة',
'collections.share.members': 'الأعضاء',
'collections.share.owner': 'المالك',
'collections.share.you': 'أنت',
'collections.share.pending': 'دعوة معلّقة',
'collections.share.accepted': 'مقبولة',
'collections.share.invite': 'دعوة شخص',
'collections.share.inviteUser': 'اختر مستخدمًا',
'collections.share.sendInvite': 'إرسال الدعوة',
'collections.share.inviteHint': 'يمكن للأشخاص المدعوين عرض هذه القائمة وتحريرها.',
'collections.share.noUsers': 'لا يوجد مستخدمون متاحون للدعوة.',
'collections.share.memberHint': 'يمكن لمالك القائمة فقط دعوة الأشخاص أو إزالتهم.',
'collections.share.cancel': 'إلغاء',
'collections.share.leave': 'مغادرة القائمة',
'collections.share.leaveConfirm': 'هل تريد مغادرة هذه القائمة المشتركة؟ ستفقد إمكانية الوصول إليها حتى تتم دعوتك مرة أخرى.',
'collections.share.left': 'لقد غادرت القائمة',
'collections.share.remove': 'إزالة',
'collections.invites.title': 'الدعوات',
+1
View File
@@ -367,6 +367,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'المجموعات',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Usar',
'collections.share.title': 'Compartilhar lista',
'collections.share.titleNamed': 'Compartilhar “{name}”',
'collections.share.button': 'Compartilhar',
'collections.share.members': 'Membros',
'collections.share.owner': 'Proprietário',
'collections.share.you': 'você',
'collections.share.pending': 'convite pendente',
'collections.share.accepted': 'aceito',
'collections.share.invite': 'Convidar alguém',
'collections.share.inviteUser': 'Selecionar usuário',
'collections.share.sendInvite': 'Enviar convite',
'collections.share.inviteHint': 'As pessoas convidadas podem ver e editar esta lista.',
'collections.share.noUsers': 'Nenhum usuário disponível para convidar.',
'collections.share.memberHint': 'Apenas o proprietário da lista pode convidar ou remover pessoas.',
'collections.share.cancel': 'Cancelar',
'collections.share.leave': 'Sair da lista',
'collections.share.leaveConfirm': 'Sair desta lista compartilhada? Você perderá o acesso até ser convidado novamente.',
'collections.share.left': 'Você saiu da lista',
'collections.share.remove': 'Remover',
'collections.invites.title': 'Convites',
+1
View File
@@ -377,6 +377,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Coleções',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Použít',
'collections.share.title': 'Sdílet seznam',
'collections.share.titleNamed': 'Sdílet „{name}“',
'collections.share.button': 'Sdílet',
'collections.share.members': 'Členové',
'collections.share.owner': 'Vlastník',
'collections.share.you': 'vy',
'collections.share.pending': 'čekající pozvánka',
'collections.share.accepted': 'přijato',
'collections.share.invite': 'Pozvat někoho',
'collections.share.inviteUser': 'Vyberte uživatele',
'collections.share.sendInvite': 'Odeslat pozvánku',
'collections.share.inviteHint': 'Pozvaní lidé mohou tento seznam zobrazit a upravovat.',
'collections.share.noUsers': 'Není k dispozici žádný uživatel k pozvání.',
'collections.share.memberHint': 'Pozvat nebo odebrat lidi může pouze vlastník seznamu.',
'collections.share.cancel': 'Zrušit',
'collections.share.leave': 'Opustit seznam',
'collections.share.leaveConfirm': 'Opustit tento sdílený seznam? Ztratíte přístup, dokud nebudete znovu pozváni.',
'collections.share.left': 'Opustili jste seznam',
'collections.share.remove': 'Odebrat',
'collections.invites.title': 'Pozvánky',
+1
View File
@@ -374,6 +374,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Sbírky',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Übernehmen',
'collections.share.title': 'Liste teilen',
'collections.share.titleNamed': '„{name}“ teilen',
'collections.share.button': 'Teilen',
'collections.share.members': 'Mitglieder',
'collections.share.owner': 'Besitzer',
'collections.share.you': 'du',
'collections.share.pending': 'ausstehende Einladung',
'collections.share.accepted': 'angenommen',
'collections.share.invite': 'Jemanden einladen',
'collections.share.inviteUser': 'Benutzer wählen',
'collections.share.sendInvite': 'Einladung senden',
'collections.share.inviteHint': 'Eingeladene Personen können diese Liste ansehen und bearbeiten.',
'collections.share.noUsers': 'Keine Benutzer zum Einladen verfügbar.',
'collections.share.memberHint': 'Nur der Listenbesitzer kann Personen einladen oder entfernen.',
'collections.share.cancel': 'Abbrechen',
'collections.share.leave': 'Liste verlassen',
'collections.share.leaveConfirm': 'Diese geteilte Liste verlassen? Du verlierst den Zugriff, bis du erneut eingeladen wirst.',
'collections.share.left': 'Du hast die Liste verlassen',
'collections.share.remove': 'Entfernen',
'collections.invites.title': 'Einladungen',
+1
View File
@@ -381,6 +381,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobil',
'settings.appearance.widget.sidebar': 'Rechte Seitenleiste',
'settings.appearance.widget.currency': 'Währung',
'settings.appearance.widget.collections': 'Sammlungen',
'settings.appearance.widget.timezones': 'Zeitzonen',
'settings.appearance.widget.upcomingReservations': 'Anstehende Reservierungen',
'settings.appearance.widget.atlas': 'Atlas / Länder',
+9 -1
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Use',
'collections.share.title': 'Share list',
'collections.share.titleNamed': 'Share “{name}”',
'collections.share.button': 'Share',
'collections.share.members': 'Members',
'collections.share.owner': 'Owner',
'collections.share.you': 'you',
'collections.share.pending': 'pending invite',
'collections.share.accepted': 'accepted',
'collections.share.invite': 'Invite someone',
'collections.share.inviteUser': 'Select user',
'collections.share.inviteUser': 'Select a user',
'collections.share.sendInvite': 'Send invite',
'collections.share.inviteHint': 'Invited people can view and edit this list.',
'collections.share.noUsers': 'No users available to invite.',
'collections.share.memberHint': 'Only the list owner can invite or remove people.',
'collections.share.cancel': 'Cancel',
'collections.share.leave': 'Leave list',
'collections.share.leaveConfirm': 'Leave this shared list? You will lose access until you are invited again.',
'collections.share.left': 'You left the list',
'collections.share.remove': 'Remove',
'collections.invites.title': 'Invites',
+1
View File
@@ -375,6 +375,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Collections',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Usar',
'collections.share.title': 'Compartir lista',
'collections.share.titleNamed': 'Compartir «{name}»',
'collections.share.button': 'Compartir',
'collections.share.members': 'Miembros',
'collections.share.owner': 'Propietario',
'collections.share.you': 'tú',
'collections.share.pending': 'invitación pendiente',
'collections.share.accepted': 'aceptada',
'collections.share.invite': 'Invitar a alguien',
'collections.share.inviteUser': 'Seleccionar usuario',
'collections.share.sendInvite': 'Enviar invitación',
'collections.share.inviteHint': 'Las personas invitadas pueden ver y editar esta lista.',
'collections.share.noUsers': 'No hay usuarios disponibles para invitar.',
'collections.share.memberHint': 'Solo el propietario de la lista puede invitar o quitar personas.',
'collections.share.cancel': 'Cancelar',
'collections.share.leave': 'Salir de la lista',
'collections.share.leaveConfirm': '¿Salir de esta lista compartida? Perderás el acceso hasta que vuelvan a invitarte.',
'collections.share.left': 'Has salido de la lista',
'collections.share.remove': 'Quitar',
'collections.invites.title': 'Invitaciones',
+1
View File
@@ -380,6 +380,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Colecciones',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Utiliser',
'collections.share.title': 'Partager la liste',
'collections.share.titleNamed': 'Partager « {name} »',
'collections.share.button': 'Partager',
'collections.share.members': 'Membres',
'collections.share.owner': 'Propriétaire',
'collections.share.you': 'vous',
'collections.share.pending': 'invitation en attente',
'collections.share.accepted': 'acceptée',
'collections.share.invite': 'Inviter quelquun',
'collections.share.inviteUser': 'Sélectionner un utilisateur',
'collections.share.sendInvite': 'Envoyer linvitation',
'collections.share.inviteHint': 'Les personnes invitées peuvent voir et modifier cette liste.',
'collections.share.noUsers': 'Aucun utilisateur à inviter.',
'collections.share.memberHint': 'Seul le propriétaire de la liste peut inviter ou retirer des personnes.',
'collections.share.cancel': 'Annuler',
'collections.share.leave': 'Quitter la liste',
'collections.share.leaveConfirm': 'Quitter cette liste partagée ? Vous perdrez laccès jusqu’à ce que vous soyez réinvité.',
'collections.share.left': 'Vous avez quitté la liste',
'collections.share.remove': 'Retirer',
'collections.invites.title': 'Invitations',
+1
View File
@@ -385,6 +385,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Collections',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Χρήση',
'collections.share.title': 'Κοινή χρήση λίστας',
'collections.share.titleNamed': 'Κοινή χρήση «{name}»',
'collections.share.button': 'Κοινή χρήση',
'collections.share.members': 'Μέλη',
'collections.share.owner': 'Ιδιοκτήτης',
'collections.share.you': 'εσύ',
'collections.share.pending': 'εκκρεμής πρόσκληση',
'collections.share.accepted': 'αποδεκτή',
'collections.share.invite': 'Προσκάλεσε κάποιον',
'collections.share.inviteUser': 'Επιλογή χρήστη',
'collections.share.sendInvite': 'Αποστολή πρόσκλησης',
'collections.share.inviteHint': 'Τα προσκεκλημένα άτομα μπορούν να δουν και να επεξεργαστούν αυτή τη λίστα.',
'collections.share.noUsers': 'Δεν υπάρχουν διαθέσιμοι χρήστες για πρόσκληση.',
'collections.share.memberHint': 'Μόνο ο ιδιοκτήτης της λίστας μπορεί να προσκαλεί ή να αφαιρεί άτομα.',
'collections.share.cancel': 'Άκυρο',
'collections.share.leave': 'Αποχώρηση από τη λίστα',
'collections.share.leaveConfirm': 'Αποχώρηση από αυτή την κοινόχρηστη λίστα; Θα χάσεις την πρόσβαση μέχρι να προσκληθείς ξανά.',
'collections.share.left': 'Αποχώρησες από τη λίστα',
'collections.share.remove': 'Αφαίρεση',
'collections.invites.title': 'Προσκλήσεις',
+1
View File
@@ -386,6 +386,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Συλλογές',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Használat',
'collections.share.title': 'Lista megosztása',
'collections.share.titleNamed': '„{name}” megosztása',
'collections.share.button': 'Megosztás',
'collections.share.members': 'Tagok',
'collections.share.owner': 'Tulajdonos',
'collections.share.you': 'te',
'collections.share.pending': 'függőben lévő meghívó',
'collections.share.accepted': 'elfogadva',
'collections.share.invite': 'Meghívás',
'collections.share.inviteUser': 'Válassz felhasználót',
'collections.share.sendInvite': 'Meghívó küldése',
'collections.share.inviteHint': 'A meghívott személyek megtekinthetik és szerkeszthetik ezt a listát.',
'collections.share.noUsers': 'Nincs meghívható felhasználó.',
'collections.share.memberHint': 'Csak a lista tulajdonosa hívhat meg vagy távolíthat el embereket.',
'collections.share.cancel': 'Mégse',
'collections.share.leave': 'Lista elhagyása',
'collections.share.leaveConfirm': 'Elhagyod ezt a megosztott listát? Elveszíted a hozzáférést, amíg újra meg nem hívnak.',
'collections.share.left': 'Elhagytad a listát',
'collections.share.remove': 'Eltávolítás',
'collections.invites.title': 'Meghívók',
+1
View File
@@ -379,6 +379,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Gyűjtemények',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Gunakan',
'collections.share.title': 'Bagikan daftar',
'collections.share.titleNamed': 'Bagikan “{name}”',
'collections.share.button': 'Bagikan',
'collections.share.members': 'Anggota',
'collections.share.owner': 'Pemilik',
'collections.share.you': 'Anda',
'collections.share.pending': 'undangan tertunda',
'collections.share.accepted': 'diterima',
'collections.share.invite': 'Undang seseorang',
'collections.share.inviteUser': 'Pilih pengguna',
'collections.share.sendInvite': 'Kirim undangan',
'collections.share.inviteHint': 'Orang yang diundang dapat melihat dan mengedit daftar ini.',
'collections.share.noUsers': 'Tidak ada pengguna yang tersedia untuk diundang.',
'collections.share.memberHint': 'Hanya pemilik daftar yang dapat mengundang atau menghapus orang.',
'collections.share.cancel': 'Batal',
'collections.share.leave': 'Tinggalkan daftar',
'collections.share.leaveConfirm': 'Tinggalkan daftar bersama ini? Anda akan kehilangan akses hingga diundang lagi.',
'collections.share.left': 'Anda meninggalkan daftar',
'collections.share.remove': 'Hapus',
'collections.invites.title': 'Undangan',
+1
View File
@@ -378,6 +378,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Koleksi',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Usa',
'collections.share.title': 'Condividi lista',
'collections.share.titleNamed': 'Condividi «{name}»',
'collections.share.button': 'Condividi',
'collections.share.members': 'Membri',
'collections.share.owner': 'Proprietario',
'collections.share.you': 'tu',
'collections.share.pending': 'invito in sospeso',
'collections.share.accepted': 'accettato',
'collections.share.invite': 'Invita qualcuno',
'collections.share.inviteUser': 'Seleziona utente',
'collections.share.sendInvite': 'Invia invito',
'collections.share.inviteHint': 'Le persone invitate possono vedere e modificare questa lista.',
'collections.share.noUsers': 'Nessun utente disponibile da invitare.',
'collections.share.memberHint': 'Solo il proprietario della lista può invitare o rimuovere persone.',
'collections.share.cancel': 'Annulla',
'collections.share.leave': 'Abbandona lista',
'collections.share.leaveConfirm': 'Abbandonare questa lista condivisa? Perderai laccesso finché non verrai invitato di nuovo.',
'collections.share.left': 'Hai abbandonato la lista',
'collections.share.remove': 'Rimuovi',
'collections.invites.title': 'Inviti',
+1
View File
@@ -378,6 +378,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Raccolte',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': '使用',
'collections.share.title': 'リストを共有',
'collections.share.titleNamed': '「{name}」を共有',
'collections.share.button': '共有',
'collections.share.members': 'メンバー',
'collections.share.owner': 'オーナー',
'collections.share.you': 'あなた',
'collections.share.pending': '招待保留中',
'collections.share.accepted': '承認済み',
'collections.share.invite': '誰かを招待',
'collections.share.inviteUser': 'ユーザーを選択',
'collections.share.sendInvite': '招待を送信',
'collections.share.inviteHint': '招待された人はこのリストを表示・編集できます。',
'collections.share.noUsers': '招待できるユーザーがいません。',
'collections.share.memberHint': 'リストへの招待やメンバーの削除ができるのはオーナーのみです。',
'collections.share.cancel': 'キャンセル',
'collections.share.leave': 'リストから退出',
'collections.share.leaveConfirm': 'この共有リストから退出しますか?再度招待されるまでアクセスできなくなります。',
'collections.share.left': 'リストから退出しました',
'collections.share.remove': '削除',
'collections.invites.title': '招待',
+1
View File
@@ -355,6 +355,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'コレクション',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': '사용',
'collections.share.title': '목록 공유',
'collections.share.titleNamed': '“{name}” 공유',
'collections.share.button': '공유',
'collections.share.members': '멤버',
'collections.share.owner': '소유자',
'collections.share.you': '나',
'collections.share.pending': '대기 중인 초대',
'collections.share.accepted': '수락됨',
'collections.share.invite': '초대하기',
'collections.share.inviteUser': '사용자 선택',
'collections.share.sendInvite': '초대 보내기',
'collections.share.inviteHint': '초대받은 사람은 이 목록을 보고 편집할 수 있습니다.',
'collections.share.noUsers': '초대할 수 있는 사용자가 없습니다.',
'collections.share.memberHint': '목록 소유자만 사람을 초대하거나 제거할 수 있습니다.',
'collections.share.cancel': '취소',
'collections.share.leave': '목록 나가기',
'collections.share.leaveConfirm': '이 공유 목록에서 나가시겠습니까? 다시 초대받을 때까지 액세스할 수 없습니다.',
'collections.share.left': '목록에서 나갔습니다',
'collections.share.remove': '제거',
'collections.invites.title': '초대',
+1
View File
@@ -370,6 +370,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': '컬렉션',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Gebruiken',
'collections.share.title': 'Lijst delen',
'collections.share.titleNamed': '“{name}” delen',
'collections.share.button': 'Delen',
'collections.share.members': 'Leden',
'collections.share.owner': 'Eigenaar',
'collections.share.you': 'jij',
'collections.share.pending': 'uitnodiging in afwachting',
'collections.share.accepted': 'geaccepteerd',
'collections.share.invite': 'Iemand uitnodigen',
'collections.share.inviteUser': 'Gebruiker selecteren',
'collections.share.sendInvite': 'Uitnodiging sturen',
'collections.share.inviteHint': 'Uitgenodigde personen kunnen deze lijst bekijken en bewerken.',
'collections.share.noUsers': 'Geen gebruikers beschikbaar om uit te nodigen.',
'collections.share.memberHint': 'Alleen de eigenaar van de lijst kan personen uitnodigen of verwijderen.',
'collections.share.cancel': 'Annuleren',
'collections.share.leave': 'Lijst verlaten',
'collections.share.leaveConfirm': 'Deze gedeelde lijst verlaten? Je verliest de toegang totdat je opnieuw wordt uitgenodigd.',
'collections.share.left': 'Je hebt de lijst verlaten',
'collections.share.remove': 'Verwijderen',
'collections.invites.title': 'Uitnodigingen',
+1
View File
@@ -378,6 +378,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Collecties',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Użyj',
'collections.share.title': 'Udostępnij listę',
'collections.share.titleNamed': 'Udostępnij „{name}”',
'collections.share.button': 'Udostępnij',
'collections.share.members': 'Członkowie',
'collections.share.owner': 'Właściciel',
'collections.share.you': 'Ty',
'collections.share.pending': 'oczekujące zaproszenie',
'collections.share.accepted': 'zaakceptowano',
'collections.share.invite': 'Zaproś kogoś',
'collections.share.inviteUser': 'Wybierz użytkownika',
'collections.share.sendInvite': 'Wyślij zaproszenie',
'collections.share.inviteHint': 'Zaproszone osoby mogą przeglądać i edytować tę listę.',
'collections.share.noUsers': 'Brak użytkowników do zaproszenia.',
'collections.share.memberHint': 'Tylko właściciel listy może zapraszać lub usuwać osoby.',
'collections.share.cancel': 'Anuluj',
'collections.share.leave': 'Opuść listę',
'collections.share.leaveConfirm': 'Opuścić tę udostępnioną listę? Utracisz dostęp, dopóki nie otrzymasz ponownego zaproszenia.',
'collections.share.left': 'Opuściłeś listę',
'collections.share.remove': 'Usuń',
'collections.invites.title': 'Zaproszenia',
+1
View File
@@ -379,6 +379,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Kolekcje',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Использовать',
'collections.share.title': 'Поделиться списком',
'collections.share.titleNamed': 'Поделиться «{name}»',
'collections.share.button': 'Поделиться',
'collections.share.members': 'Участники',
'collections.share.owner': 'Владелец',
'collections.share.you': 'вы',
'collections.share.pending': 'ожидает приглашения',
'collections.share.accepted': 'принято',
'collections.share.invite': 'Пригласить',
'collections.share.inviteUser': 'Выберите пользователя',
'collections.share.sendInvite': 'Отправить приглашение',
'collections.share.inviteHint': 'Приглашённые могут просматривать и редактировать этот список.',
'collections.share.noUsers': 'Нет пользователей для приглашения.',
'collections.share.memberHint': 'Только владелец списка может приглашать или удалять участников.',
'collections.share.cancel': 'Отмена',
'collections.share.leave': 'Покинуть список',
'collections.share.leaveConfirm': 'Покинуть этот общий список? Вы потеряете доступ, пока вас не пригласят снова.',
'collections.share.left': 'Вы покинули список',
'collections.share.remove': 'Удалить',
'collections.invites.title': 'Приглашения',
+1
View File
@@ -380,6 +380,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Коллекции',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Använd',
'collections.share.title': 'Dela lista',
'collections.share.titleNamed': 'Dela ”{name}”',
'collections.share.button': 'Dela',
'collections.share.members': 'Medlemmar',
'collections.share.owner': 'Ägare',
'collections.share.you': 'du',
'collections.share.pending': 'väntande inbjudan',
'collections.share.accepted': 'accepterad',
'collections.share.invite': 'Bjud in någon',
'collections.share.inviteUser': 'Välj användare',
'collections.share.sendInvite': 'Skicka inbjudan',
'collections.share.inviteHint': 'Inbjudna personer kan visa och redigera den här listan.',
'collections.share.noUsers': 'Inga användare tillgängliga att bjuda in.',
'collections.share.memberHint': 'Endast listans ägare kan bjuda in eller ta bort personer.',
'collections.share.cancel': 'Avbryt',
'collections.share.leave': 'Lämna lista',
'collections.share.leaveConfirm': 'Lämna den här delade listan? Du förlorar åtkomsten tills du blir inbjuden igen.',
'collections.share.left': 'Du lämnade listan',
'collections.share.remove': 'Ta bort',
'collections.invites.title': 'Inbjudningar',
+1
View File
@@ -375,6 +375,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Samlingar',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Kullan',
'collections.share.title': 'Listeyi paylaş',
'collections.share.titleNamed': '“{name}” listesini paylaş',
'collections.share.button': 'Paylaş',
'collections.share.members': 'Üyeler',
'collections.share.owner': 'Sahip',
'collections.share.you': 'sen',
'collections.share.pending': 'bekleyen davet',
'collections.share.accepted': 'kabul edildi',
'collections.share.invite': 'Birini davet et',
'collections.share.inviteUser': 'Kullanıcı seç',
'collections.share.sendInvite': 'Davet gönder',
'collections.share.inviteHint': 'Davet edilen kişiler bu listeyi görüntüleyebilir ve düzenleyebilir.',
'collections.share.noUsers': 'Davet edilecek kullanıcı yok.',
'collections.share.memberHint': 'Yalnızca liste sahibi kişileri davet edebilir veya kaldırabilir.',
'collections.share.cancel': 'İptal',
'collections.share.leave': 'Listeden ayrıl',
'collections.share.leaveConfirm': 'Bu paylaşılan listeden ayrılmak istiyor musunuz? Tekrar davet edilene kadar erişiminizi kaybedersiniz.',
'collections.share.left': 'Listeden ayrıldınız',
'collections.share.remove': 'Kaldır',
'collections.invites.title': 'Davetler',
+1
View File
@@ -375,6 +375,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Koleksiyonlar',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Використати',
'collections.share.title': 'Поділитися списком',
'collections.share.titleNamed': 'Поділитися «{name}»',
'collections.share.button': 'Поділитися',
'collections.share.members': 'Учасники',
'collections.share.owner': 'Власник',
'collections.share.you': 'ви',
'collections.share.pending': 'очікує запрошення',
'collections.share.accepted': 'прийнято',
'collections.share.invite': 'Запросити',
'collections.share.inviteUser': 'Виберіть користувача',
'collections.share.sendInvite': 'Надіслати запрошення',
'collections.share.inviteHint': 'Запрошені можуть переглядати та редагувати цей список.',
'collections.share.noUsers': 'Немає користувачів для запрошення.',
'collections.share.memberHint': 'Лише власник списку може запрошувати або вилучати учасників.',
'collections.share.cancel': 'Скасувати',
'collections.share.leave': 'Покинути список',
'collections.share.leaveConfirm': 'Покинути цей спільний список? Ви втратите доступ, доки вас не запросять знову.',
'collections.share.left': 'Ви покинули список',
'collections.share.remove': 'Видалити',
'collections.invites.title': 'Запрошення',
+1
View File
@@ -379,6 +379,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': 'Колекції',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': 'Dùng',
'collections.share.title': 'Chia sẻ danh sách',
'collections.share.titleNamed': 'Chia sẻ “{name}”',
'collections.share.button': 'Chia sẻ',
'collections.share.members': 'Thành viên',
'collections.share.owner': 'Chủ sở hữu',
'collections.share.you': 'bạn',
'collections.share.pending': 'lời mời đang chờ',
'collections.share.accepted': 'đã chấp nhận',
'collections.share.invite': 'Mời ai đó',
'collections.share.inviteUser': 'Chọn người dùng',
'collections.share.sendInvite': 'Gửi lời mời',
'collections.share.inviteHint': 'Người được mời có thể xem và chỉnh sửa danh sách này.',
'collections.share.noUsers': 'Không có người dùng nào để mời.',
'collections.share.memberHint': 'Chỉ chủ sở hữu danh sách mới có thể mời hoặc xóa người.',
'collections.share.cancel': 'Hủy',
'collections.share.leave': 'Rời khỏi danh sách',
'collections.share.leaveConfirm': 'Rời khỏi danh sách được chia sẻ này? Bạn sẽ mất quyền truy cập cho đến khi được mời lại.',
'collections.share.left': 'Bạn đã rời khỏi danh sách',
'collections.share.remove': 'Xóa',
'collections.invites.title': 'Lời mời',
+1
View File
@@ -377,6 +377,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Di động',
'settings.appearance.widget.sidebar': 'Thanh bên phải',
'settings.appearance.widget.currency': 'Tiền tệ',
'settings.appearance.widget.collections': 'Bộ sưu tập',
'settings.appearance.widget.timezones': 'Múi giờ',
'settings.appearance.widget.upcomingReservations': 'Đặt chỗ sắp tới',
'settings.appearance.widget.atlas': 'Atlas / quốc gia',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': '使用',
'collections.share.title': '分享清單',
'collections.share.titleNamed': '分享「{name}」',
'collections.share.button': '分享',
'collections.share.members': '成員',
'collections.share.owner': '擁有者',
'collections.share.you': '你',
'collections.share.pending': '待處理邀請',
'collections.share.accepted': '已接受',
'collections.share.invite': '邀請他人',
'collections.share.inviteUser': '選擇使用者',
'collections.share.sendInvite': '傳送邀請',
'collections.share.inviteHint': '受邀者可以檢視和編輯此清單。',
'collections.share.noUsers': '沒有可邀請的使用者。',
'collections.share.memberHint': '只有清單擁有者才能邀請或移除成員。',
'collections.share.cancel': '取消',
'collections.share.leave': '退出清單',
'collections.share.leaveConfirm': '退出此分享清單?在再次收到邀請之前,你將無法存取。',
'collections.share.left': '你已退出清單',
'collections.share.remove': '移除',
'collections.invites.title': '邀請',
+1
View File
@@ -359,6 +359,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': '收藏',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',
+8
View File
@@ -72,15 +72,23 @@ const collection: TranslationStrings = {
'collections.picker.use': '使用',
'collections.share.title': '共享列表',
'collections.share.titleNamed': '共享“{name}”',
'collections.share.button': '共享',
'collections.share.members': '成员',
'collections.share.owner': '所有者',
'collections.share.you': '你',
'collections.share.pending': '待处理邀请',
'collections.share.accepted': '已接受',
'collections.share.invite': '邀请他人',
'collections.share.inviteUser': '选择用户',
'collections.share.sendInvite': '发送邀请',
'collections.share.inviteHint': '被邀请的人可以查看和编辑此列表。',
'collections.share.noUsers': '没有可邀请的用户。',
'collections.share.memberHint': '只有列表所有者才能邀请或移除成员。',
'collections.share.cancel': '取消',
'collections.share.leave': '退出列表',
'collections.share.leaveConfirm': '退出此共享列表?在再次收到邀请之前,你将无法访问。',
'collections.share.left': '你已退出列表',
'collections.share.remove': '移除',
'collections.invites.title': '邀请',
+1
View File
@@ -357,6 +357,7 @@ const settings: TranslationStrings = {
'settings.appearance.mobile': 'Mobile',
'settings.appearance.widget.sidebar': 'Right sidebar',
'settings.appearance.widget.currency': 'Currency',
'settings.appearance.widget.collections': '收藏',
'settings.appearance.widget.timezones': 'Timezones',
'settings.appearance.widget.upcomingReservations': 'Upcoming reservations',
'settings.appearance.widget.atlas': 'Atlas / countries',