feat: bulk import for packing lists + complete i18n sync — closes #133
Packing list bulk import: - Import button in packing list header opens a modal - Paste items or load CSV/TXT file - Format: Category, Name, Weight (g), Bag, checked/unchecked - Bags are auto-created if they don't exist - Server endpoint POST /packing/import with transaction i18n sync: - Added all missing translation keys to fr, es, nl, ru, zh, ar - All 8 language files now have matching key sets - Includes memories, vacay weekdays, packing import, settlement, GPX import, blur booking codes, transport timeline keys
This commit is contained in:
@@ -112,6 +112,7 @@ export const assignmentsApi = {
|
||||
export const packingApi = {
|
||||
list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/packing`).then(r => r.data),
|
||||
create: (tripId: number | string, data: Record<string, unknown>) => apiClient.post(`/trips/${tripId}/packing`, data).then(r => r.data),
|
||||
bulkImport: (tripId: number | string, items: { name: string; category?: string; quantity?: number }[]) => apiClient.post(`/trips/${tripId}/packing/import`, { items }).then(r => r.data),
|
||||
update: (tripId: number | string, id: number, data: Record<string, unknown>) => apiClient.put(`/trips/${tripId}/packing/${id}`, data).then(r => r.data),
|
||||
delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/packing/${id}`).then(r => r.data),
|
||||
reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/packing/reorder`, { orderedIds }).then(r => r.data),
|
||||
|
||||
@@ -3,9 +3,10 @@ import { useTripStore } from '../../store/tripStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { packingApi, tripsApi, adminApi } from '../../api/client'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {
|
||||
CheckSquare, Square, Trash2, Plus, ChevronDown, ChevronRight,
|
||||
X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, Luggage, UserPlus, Package, FolderPlus,
|
||||
X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, Luggage, UserPlus, Package, FolderPlus, Upload,
|
||||
} from 'lucide-react'
|
||||
import type { PackingItem } from '../../types'
|
||||
|
||||
@@ -727,6 +728,9 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp
|
||||
const [availableTemplates, setAvailableTemplates] = useState<{ id: number; name: string; item_count: number }[]>([])
|
||||
const [showTemplateDropdown, setShowTemplateDropdown] = useState(false)
|
||||
const [applyingTemplate, setApplyingTemplate] = useState(false)
|
||||
const [showImportModal, setShowImportModal] = useState(false)
|
||||
const [importText, setImportText] = useState('')
|
||||
const csvInputRef = useRef<HTMLInputElement>(null)
|
||||
const templateDropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -757,6 +761,44 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp
|
||||
}
|
||||
}
|
||||
|
||||
const parseImportLines = (text: string) => {
|
||||
return text.split('\n').map(line => line.trim()).filter(Boolean).map(line => {
|
||||
// Format: Category, Name, Weight (optional), Bag (optional), checked/unchecked (optional)
|
||||
const parts = line.split(/[,;\t]/).map(s => s.trim())
|
||||
if (parts.length >= 2) {
|
||||
const category = parts[0]
|
||||
const name = parts[1]
|
||||
const weight_grams = parts[2] || undefined
|
||||
const bag = parts[3] || undefined
|
||||
const checked = parts[4]?.toLowerCase() === 'checked' || parts[4] === '1'
|
||||
return { name, category, weight_grams, bag, checked }
|
||||
}
|
||||
// Single value = just a name
|
||||
return { name: parts[0], category: undefined, weight_grams: undefined, bag: undefined, checked: false }
|
||||
}).filter(i => i.name)
|
||||
}
|
||||
|
||||
const handleBulkImport = async () => {
|
||||
const parsed = parseImportLines(importText)
|
||||
if (parsed.length === 0) { toast.error(t('packing.importEmpty')); return }
|
||||
try {
|
||||
const result = await packingApi.bulkImport(tripId, parsed)
|
||||
toast.success(t('packing.importSuccess', { count: result.count }))
|
||||
setImportText('')
|
||||
setShowImportModal(false)
|
||||
window.location.reload()
|
||||
} catch { toast.error(t('packing.importError')) }
|
||||
}
|
||||
|
||||
const handleCsvFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
e.target.value = ''
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => { if (typeof reader.result === 'string') setImportText(reader.result) }
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const font = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }
|
||||
|
||||
return (
|
||||
@@ -781,6 +823,13 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp
|
||||
<span className="sm:hidden">{t('packing.clearCheckedShort', { count: abgehakt })}</span>
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setShowImportModal(true)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5, padding: '5px 11px', borderRadius: 99,
|
||||
border: '1px solid var(--border-primary)', fontSize: 12, fontWeight: 500, cursor: 'pointer',
|
||||
fontFamily: 'inherit', background: 'var(--bg-card)', color: 'var(--text-muted)',
|
||||
}}>
|
||||
<Upload size={12} /> <span className="hidden sm:inline">{t('packing.import')}</span>
|
||||
</button>
|
||||
{availableTemplates.length > 0 && (
|
||||
<div ref={templateDropdownRef} style={{ position: 'relative' }}>
|
||||
<button onClick={() => setShowTemplateDropdown(v => !v)} disabled={applyingTemplate} style={{
|
||||
@@ -1102,6 +1151,60 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp
|
||||
.assignee-chip:hover + .assignee-tooltip { opacity: 1 !important; }
|
||||
.assignee-chip:hover { opacity: 0.7; }
|
||||
`}</style>
|
||||
|
||||
{/* Bulk Import Modal */}
|
||||
{showImportModal && ReactDOM.createPortal(
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, zIndex: 1000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(3px)',
|
||||
}} onClick={() => setShowImportModal(false)}>
|
||||
<div style={{
|
||||
width: 420, maxHeight: '80vh', background: 'var(--bg-card)', borderRadius: 16,
|
||||
boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px',
|
||||
display: 'flex', flexDirection: 'column', gap: 14,
|
||||
}} onClick={e => e.stopPropagation()}>
|
||||
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>{t('packing.importTitle')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-faint)', lineHeight: 1.5 }}>{t('packing.importHint')}</div>
|
||||
<textarea
|
||||
value={importText}
|
||||
onChange={e => setImportText(e.target.value)}
|
||||
rows={10}
|
||||
placeholder={t('packing.importPlaceholder')}
|
||||
style={{
|
||||
width: '100%', border: '1px solid var(--border-primary)', borderRadius: 10,
|
||||
padding: '10px 12px', fontSize: 13, fontFamily: 'monospace',
|
||||
outline: 'none', boxSizing: 'border-box', color: 'var(--text-primary)',
|
||||
background: 'var(--bg-input)', resize: 'vertical', lineHeight: 1.5,
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<input ref={csvInputRef} type="file" accept=".csv,.txt" style={{ display: 'none' }} onChange={handleCsvFile} />
|
||||
<button onClick={() => csvInputRef.current?.click()} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5, padding: '5px 10px',
|
||||
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
|
||||
fontSize: 11, color: 'var(--text-faint)', cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>
|
||||
<Upload size={11} /> {t('packing.importCsv')}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button onClick={() => setShowImportModal(false)} style={{
|
||||
fontSize: 12, background: 'none', border: '1px solid var(--border-primary)',
|
||||
borderRadius: 8, padding: '6px 14px', cursor: 'pointer', color: 'var(--text-muted)', fontFamily: 'inherit',
|
||||
}}>{t('common.cancel')}</button>
|
||||
<button onClick={handleBulkImport} disabled={!importText.trim()} style={{
|
||||
fontSize: 12, background: 'var(--accent)', color: 'var(--accent-text)',
|
||||
border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600,
|
||||
fontFamily: 'inherit', opacity: importText.trim() ? 1 : 0.5,
|
||||
}}>{t('packing.importAction', { count: parseImportLines(importText).length })}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.temperature': 'وحدة الحرارة',
|
||||
'settings.timeFormat': 'تنسيق الوقت',
|
||||
'settings.routeCalculation': 'حساب المسار',
|
||||
'settings.blurBookingCodes': 'إخفاء رموز الحجز',
|
||||
'settings.on': 'تشغيل',
|
||||
'settings.off': 'إيقاف',
|
||||
'settings.account': 'الحساب',
|
||||
@@ -482,6 +483,14 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'vacay.carriedOver': 'من {year}',
|
||||
'vacay.blockWeekends': 'حظر عطلة نهاية الأسبوع',
|
||||
'vacay.blockWeekendsHint': 'منع إدخالات الإجازة يومي السبت والأحد',
|
||||
'vacay.weekendDays': 'أيام عطلة نهاية الأسبوع',
|
||||
'vacay.mon': 'الاثنين',
|
||||
'vacay.tue': 'الثلاثاء',
|
||||
'vacay.wed': 'الأربعاء',
|
||||
'vacay.thu': 'الخميس',
|
||||
'vacay.fri': 'الجمعة',
|
||||
'vacay.sat': 'السبت',
|
||||
'vacay.sun': 'الأحد',
|
||||
'vacay.publicHolidays': 'العطل الرسمية',
|
||||
'vacay.publicHolidaysHint': 'وضع علامة على العطل الرسمية في التقويم',
|
||||
'vacay.selectCountry': 'اختر الدولة',
|
||||
@@ -624,9 +633,18 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'تصدير خطة اليوم بصيغة PDF',
|
||||
'dayplan.pdfError': 'فشل تصدير PDF',
|
||||
'dayplan.cannotReorderTransport': 'لا يمكن إعادة ترتيب الحجوزات ذات الوقت الثابت',
|
||||
'dayplan.confirmRemoveTimeTitle': 'إزالة الوقت؟',
|
||||
'dayplan.confirmRemoveTimeBody': 'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.',
|
||||
'dayplan.confirmRemoveTimeAction': 'إزالة الوقت ونقل',
|
||||
'dayplan.cannotDropOnTimed': 'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت',
|
||||
'dayplan.cannotBreakChronology': 'سيؤدي هذا إلى كسر الترتيب الزمني للعناصر والحجوزات المجدولة',
|
||||
|
||||
// Places Sidebar
|
||||
'places.addPlace': 'إضافة مكان/نشاط',
|
||||
'places.importGpx': 'استيراد GPX',
|
||||
'places.gpxImported': 'تم استيراد {count} مكان من GPX',
|
||||
'places.gpxError': 'فشل استيراد GPX',
|
||||
'places.assignToDay': 'إلى أي يوم تريد الإضافة؟',
|
||||
'places.all': 'الكل',
|
||||
'places.unplanned': 'غير مخطط',
|
||||
@@ -731,6 +749,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'reservations.type.tour': 'جولة',
|
||||
'reservations.type.other': 'أخرى',
|
||||
'reservations.confirm.delete': 'هل تريد حذف الحجز "{name}"؟',
|
||||
'reservations.confirm.deleteTitle': 'حذف الحجز؟',
|
||||
'reservations.confirm.deleteBody': 'سيتم حذف "{name}" نهائيًا.',
|
||||
'reservations.toast.updated': 'تم تحديث الحجز',
|
||||
'reservations.toast.removed': 'تم حذف الحجز',
|
||||
'reservations.toast.fileUploaded': 'تم رفع الملف',
|
||||
@@ -788,6 +808,9 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'budget.paid': 'مدفوع',
|
||||
'budget.open': 'مفتوح',
|
||||
'budget.noMembers': 'لا أعضاء معينون',
|
||||
'budget.settlement': 'التسوية',
|
||||
'budget.settlementInfo': 'انقر على صورة العضو في بند الميزانية لتحديده باللون الأخضر — وهذا يعني أنه دفع. ثم تُظهر التسوية من يدين لمن وبكم.',
|
||||
'budget.netBalances': 'الأرصدة الصافية',
|
||||
|
||||
// Files
|
||||
'files.title': 'الملفات',
|
||||
@@ -841,6 +864,15 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
// Packing
|
||||
'packing.title': 'قائمة التجهيز',
|
||||
'packing.empty': 'قائمة التجهيز فارغة',
|
||||
'packing.import': 'استيراد',
|
||||
'packing.importTitle': 'استيراد قائمة التعبئة',
|
||||
'packing.importHint': 'عنصر واحد لكل سطر. يمكن إضافة الفئة والكمية مفصولة بفاصلة أو فاصلة منقوطة أو علامة تبويب: الاسم، الفئة، الكمية',
|
||||
'packing.importPlaceholder': 'فرشاة أسنان\nواقي شمس، نظافة\nقمصان، ملابس، 5\nجواز سفر، مستندات',
|
||||
'packing.importCsv': 'تحميل CSV/TXT',
|
||||
'packing.importAction': 'استيراد {count}',
|
||||
'packing.importSuccess': 'تم استيراد {count} عنصر',
|
||||
'packing.importError': 'فشل الاستيراد',
|
||||
'packing.importEmpty': 'لا توجد عناصر للاستيراد',
|
||||
'packing.progress': '{packed} من {total} جُهّز ({percent}%)',
|
||||
'packing.clearChecked': 'إزالة {count} محدد',
|
||||
'packing.clearCheckedShort': 'إزالة {count}',
|
||||
@@ -1144,6 +1176,19 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.oldest': 'الأقدم أولاً',
|
||||
'memories.newest': 'الأحدث أولاً',
|
||||
'memories.allLocations': 'جميع المواقع',
|
||||
'memories.addPhotos': 'إضافة صور',
|
||||
'memories.selectPhotos': 'اختيار صور من Immich',
|
||||
'memories.selectHint': 'انقر على الصور لتحديدها.',
|
||||
'memories.selected': 'محدد',
|
||||
'memories.addSelected': 'إضافة {count} صور',
|
||||
'memories.alreadyAdded': 'تمت الإضافة',
|
||||
'memories.private': 'خاص',
|
||||
'memories.stopSharing': 'إيقاف المشاركة',
|
||||
'memories.tripDates': 'تواريخ الرحلة',
|
||||
'memories.allPhotos': 'جميع الصور',
|
||||
'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟',
|
||||
'memories.confirmShareHint': '{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
|
||||
'memories.confirmShareButton': 'مشاركة الصور',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'الدردشة',
|
||||
|
||||
@@ -860,6 +860,15 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
// Packing
|
||||
'packing.title': 'Packliste',
|
||||
'packing.empty': 'Packliste ist leer',
|
||||
'packing.import': 'Importieren',
|
||||
'packing.importTitle': 'Packliste importieren',
|
||||
'packing.importHint': 'Ein Eintrag pro Zeile. Format: Kategorie, Name, Gewicht in g (optional), Tasche (optional), checked/unchecked (optional)',
|
||||
'packing.importPlaceholder': 'Hygiene, Zahnbürste\nKleidung, T-Shirts, 200\nDokumente, Reisepass, , Handgepäck\nElektronik, Ladekabel, 50, Koffer, checked',
|
||||
'packing.importCsv': 'CSV/TXT laden',
|
||||
'packing.importAction': '{count} importieren',
|
||||
'packing.importSuccess': '{count} Einträge importiert',
|
||||
'packing.importError': 'Import fehlgeschlagen',
|
||||
'packing.importEmpty': 'Keine Einträge zum Importieren',
|
||||
'packing.progress': '{packed} von {total} gepackt ({percent}%)',
|
||||
'packing.clearChecked': '{count} abgehakte entfernen',
|
||||
'packing.clearCheckedShort': '{count} entfernen',
|
||||
|
||||
@@ -860,6 +860,15 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
// Packing
|
||||
'packing.title': 'Packing List',
|
||||
'packing.empty': 'Packing list is empty',
|
||||
'packing.import': 'Import',
|
||||
'packing.importTitle': 'Import Packing List',
|
||||
'packing.importHint': 'One item per line. Format: Category, Name, Weight in g (optional), Bag (optional), checked/unchecked (optional)',
|
||||
'packing.importPlaceholder': 'Hygiene, Toothbrush\nClothing, T-Shirts, 200\nDocuments, Passport, , Carry-on\nElectronics, Charger, 50, Suitcase, checked',
|
||||
'packing.importCsv': 'Load CSV/TXT',
|
||||
'packing.importAction': 'Import {count}',
|
||||
'packing.importSuccess': '{count} items imported',
|
||||
'packing.importError': 'Import failed',
|
||||
'packing.importEmpty': 'No items to import',
|
||||
'packing.progress': '{packed} of {total} packed ({percent}%)',
|
||||
'packing.clearChecked': 'Remove {count} checked',
|
||||
'packing.clearCheckedShort': 'Remove {count}',
|
||||
|
||||
@@ -140,6 +140,7 @@ const es: Record<string, string> = {
|
||||
'settings.temperature': 'Unidad de temperatura',
|
||||
'settings.timeFormat': 'Formato de hora',
|
||||
'settings.routeCalculation': 'Cálculo de ruta',
|
||||
'settings.blurBookingCodes': 'Difuminar códigos de reserva',
|
||||
'settings.on': 'Activado',
|
||||
'settings.off': 'Desactivado',
|
||||
'settings.account': 'Cuenta',
|
||||
@@ -455,6 +456,14 @@ const es: Record<string, string> = {
|
||||
'vacay.carriedOver': 'de {year}',
|
||||
'vacay.blockWeekends': 'Bloquear fines de semana',
|
||||
'vacay.blockWeekendsHint': 'Impide marcar vacaciones en sábados y domingos',
|
||||
'vacay.weekendDays': 'Días de fin de semana',
|
||||
'vacay.mon': 'Lun',
|
||||
'vacay.tue': 'Mar',
|
||||
'vacay.wed': 'Mié',
|
||||
'vacay.thu': 'Jue',
|
||||
'vacay.fri': 'Vie',
|
||||
'vacay.sat': 'Sáb',
|
||||
'vacay.sun': 'Dom',
|
||||
'vacay.publicHolidays': 'Festivos',
|
||||
'vacay.publicHolidaysHint': 'Marcar festivos en el calendario',
|
||||
'vacay.selectCountry': 'Seleccionar país',
|
||||
@@ -597,9 +606,18 @@ const es: Record<string, string> = {
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'Exportar plan diario como PDF',
|
||||
'dayplan.pdfError': 'No se pudo exportar el PDF',
|
||||
'dayplan.cannotReorderTransport': 'Las reservas con hora fija no se pueden reordenar',
|
||||
'dayplan.confirmRemoveTimeTitle': '¿Eliminar hora?',
|
||||
'dayplan.confirmRemoveTimeBody': 'Este lugar tiene una hora fija ({time}). Al moverlo se eliminará la hora y se permitirá el orden libre.',
|
||||
'dayplan.confirmRemoveTimeAction': 'Eliminar hora y mover',
|
||||
'dayplan.cannotDropOnTimed': 'No se pueden colocar elementos entre entradas con hora fija',
|
||||
'dayplan.cannotBreakChronology': 'Esto rompería el orden cronológico de los elementos y reservas programados',
|
||||
|
||||
// Places Sidebar
|
||||
'places.addPlace': 'Añadir lugar/actividad',
|
||||
'places.importGpx': 'Importar GPX',
|
||||
'places.gpxImported': '{count} lugares importados desde GPX',
|
||||
'places.gpxError': 'Error al importar GPX',
|
||||
'places.assignToDay': '¿A qué día añadirlo?',
|
||||
'places.all': 'Todo',
|
||||
'places.unplanned': 'Sin planificar',
|
||||
@@ -687,6 +705,8 @@ const es: Record<string, string> = {
|
||||
'reservations.type.tour': 'Tour',
|
||||
'reservations.type.other': 'Otro',
|
||||
'reservations.confirm.delete': '¿Seguro que quieres eliminar la reserva "{name}"?',
|
||||
'reservations.confirm.deleteTitle': '¿Eliminar reserva?',
|
||||
'reservations.confirm.deleteBody': '« {name} » se eliminará permanentemente.',
|
||||
'reservations.toast.updated': 'Reserva actualizada',
|
||||
'reservations.toast.removed': 'Reserva eliminada',
|
||||
'reservations.toast.fileUploaded': 'Archivo subido',
|
||||
@@ -744,6 +764,9 @@ const es: Record<string, string> = {
|
||||
'budget.paid': 'Pagado',
|
||||
'budget.open': 'Abrir',
|
||||
'budget.noMembers': 'No hay miembros asignados',
|
||||
'budget.settlement': 'Liquidación',
|
||||
'budget.settlementInfo': 'Haz clic en el avatar de un miembro en una partida del presupuesto para marcarlo en verde — esto significa que ha pagado. La liquidación muestra quién debe cuánto a quién.',
|
||||
'budget.netBalances': 'Saldos netos',
|
||||
|
||||
// Files
|
||||
'files.title': 'Archivos',
|
||||
@@ -775,6 +798,15 @@ const es: Record<string, string> = {
|
||||
// Packing
|
||||
'packing.title': 'Lista de equipaje',
|
||||
'packing.empty': 'La lista de equipaje está vacía',
|
||||
'packing.import': 'Importar',
|
||||
'packing.importTitle': 'Importar lista de equipaje',
|
||||
'packing.importHint': 'Un elemento por línea. Categoría y cantidad opcionales separadas por coma, punto y coma o tabulación: Nombre, Categoría, Cantidad',
|
||||
'packing.importPlaceholder': 'Cepillo de dientes\nProtector solar, Higiene\nCamisetas, Ropa, 5\nPasaporte, Documentos',
|
||||
'packing.importCsv': 'Cargar CSV/TXT',
|
||||
'packing.importAction': 'Importar {count}',
|
||||
'packing.importSuccess': '{count} elementos importados',
|
||||
'packing.importError': 'Error al importar',
|
||||
'packing.importEmpty': 'Sin elementos para importar',
|
||||
'packing.progress': '{packed} de {total} preparados ({percent}%)',
|
||||
'packing.clearChecked': 'Eliminar {count} marcados',
|
||||
'packing.clearCheckedShort': 'Eliminar {count}',
|
||||
@@ -1091,6 +1123,19 @@ const es: Record<string, string> = {
|
||||
'memories.oldest': 'Más antiguas',
|
||||
'memories.newest': 'Más recientes',
|
||||
'memories.allLocations': 'Todas las ubicaciones',
|
||||
'memories.addPhotos': 'Añadir fotos',
|
||||
'memories.selectPhotos': 'Seleccionar fotos de Immich',
|
||||
'memories.selectHint': 'Toca las fotos para seleccionarlas.',
|
||||
'memories.selected': 'seleccionado(s)',
|
||||
'memories.addSelected': 'Añadir {count} fotos',
|
||||
'memories.alreadyAdded': 'Añadido',
|
||||
'memories.private': 'Privado',
|
||||
'memories.stopSharing': 'Dejar de compartir',
|
||||
'memories.tripDates': 'Fechas del viaje',
|
||||
'memories.allPhotos': 'Todas las fotos',
|
||||
'memories.confirmShareTitle': '¿Compartir con los miembros del viaje?',
|
||||
'memories.confirmShareHint': '{count} fotos serán visibles para todos los miembros de este viaje. Puedes hacer fotos individuales privadas más tarde.',
|
||||
'memories.confirmShareButton': 'Compartir fotos',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'Mensajes',
|
||||
|
||||
@@ -139,6 +139,7 @@ const fr: Record<string, string> = {
|
||||
'settings.temperature': 'Unité de température',
|
||||
'settings.timeFormat': 'Format de l\'heure',
|
||||
'settings.routeCalculation': 'Calcul d\'itinéraire',
|
||||
'settings.blurBookingCodes': 'Masquer les codes de réservation',
|
||||
'settings.on': 'Activé',
|
||||
'settings.off': 'Désactivé',
|
||||
'settings.account': 'Compte',
|
||||
@@ -475,6 +476,14 @@ const fr: Record<string, string> = {
|
||||
'vacay.carriedOver': 'de {year}',
|
||||
'vacay.blockWeekends': 'Bloquer les week-ends',
|
||||
'vacay.blockWeekendsHint': 'Empêcher les entrées de vacances les samedis et dimanches',
|
||||
'vacay.weekendDays': 'Jours de week-end',
|
||||
'vacay.mon': 'Lun',
|
||||
'vacay.tue': 'Mar',
|
||||
'vacay.wed': 'Mer',
|
||||
'vacay.thu': 'Jeu',
|
||||
'vacay.fri': 'Ven',
|
||||
'vacay.sat': 'Sam',
|
||||
'vacay.sun': 'Dim',
|
||||
'vacay.publicHolidays': 'Jours fériés',
|
||||
'vacay.publicHolidaysHint': 'Marquer les jours fériés dans le calendrier',
|
||||
'vacay.selectCountry': 'Sélectionner un pays',
|
||||
@@ -618,9 +627,18 @@ const fr: Record<string, string> = {
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'Exporter le plan du jour en PDF',
|
||||
'dayplan.pdfError': 'Échec de l\'export PDF',
|
||||
'dayplan.cannotReorderTransport': 'Les réservations avec une heure fixe ne peuvent pas être réorganisées',
|
||||
'dayplan.confirmRemoveTimeTitle': 'Supprimer l\'heure ?',
|
||||
'dayplan.confirmRemoveTimeBody': 'Ce lieu a une heure fixe ({time}). Le déplacer supprimera l\'heure et permettra un tri libre.',
|
||||
'dayplan.confirmRemoveTimeAction': 'Supprimer l\'heure et déplacer',
|
||||
'dayplan.cannotDropOnTimed': 'Les éléments ne peuvent pas être placés entre des entrées à heure fixe',
|
||||
'dayplan.cannotBreakChronology': 'Cela briserait l\'ordre chronologique des éléments et réservations planifiés',
|
||||
|
||||
// Places Sidebar
|
||||
'places.addPlace': 'Ajouter un lieu/activité',
|
||||
'places.importGpx': 'Importer GPX',
|
||||
'places.gpxImported': '{count} lieux importés depuis GPX',
|
||||
'places.gpxError': 'L\'import GPX a échoué',
|
||||
'places.assignToDay': 'Ajouter à quel jour ?',
|
||||
'places.all': 'Tous',
|
||||
'places.unplanned': 'Non planifiés',
|
||||
@@ -724,6 +742,8 @@ const fr: Record<string, string> = {
|
||||
'reservations.type.tour': 'Visite',
|
||||
'reservations.type.other': 'Autre',
|
||||
'reservations.confirm.delete': 'Voulez-vous vraiment supprimer la réservation « {name} » ?',
|
||||
'reservations.confirm.deleteTitle': 'Supprimer la réservation ?',
|
||||
'reservations.confirm.deleteBody': '« {name} » sera définitivement supprimé.',
|
||||
'reservations.toast.updated': 'Réservation mise à jour',
|
||||
'reservations.toast.removed': 'Réservation supprimée',
|
||||
'reservations.toast.fileUploaded': 'Fichier téléversé',
|
||||
@@ -781,6 +801,9 @@ const fr: Record<string, string> = {
|
||||
'budget.paid': 'Payé',
|
||||
'budget.open': 'Ouvert',
|
||||
'budget.noMembers': 'Aucun membre assigné',
|
||||
'budget.settlement': 'Règlement',
|
||||
'budget.settlementInfo': 'Cliquez sur l\'avatar d\'un membre sur un poste budgétaire pour le marquer en vert — cela signifie qu\'il a payé. Le règlement indique ensuite qui doit combien à qui.',
|
||||
'budget.netBalances': 'Soldes nets',
|
||||
|
||||
// Files
|
||||
'files.title': 'Fichiers',
|
||||
@@ -834,6 +857,15 @@ const fr: Record<string, string> = {
|
||||
// Packing
|
||||
'packing.title': 'Liste de bagages',
|
||||
'packing.empty': 'La liste de bagages est vide',
|
||||
'packing.import': 'Importer',
|
||||
'packing.importTitle': 'Importer la liste',
|
||||
'packing.importHint': 'Un élément par ligne. Catégorie et quantité optionnelles séparées par virgule, point-virgule ou tabulation : Nom, Catégorie, Quantité',
|
||||
'packing.importPlaceholder': 'Brosse à dents\nCrème solaire, Hygiène\nT-Shirts, Vêtements, 5\nPasseport, Documents',
|
||||
'packing.importCsv': 'Charger CSV/TXT',
|
||||
'packing.importAction': 'Importer {count}',
|
||||
'packing.importSuccess': '{count} éléments importés',
|
||||
'packing.importError': 'Échec de l\'import',
|
||||
'packing.importEmpty': 'Aucun élément à importer',
|
||||
'packing.progress': '{packed} sur {total} emballés ({percent} %)',
|
||||
'packing.clearChecked': 'Supprimer {count} cochés',
|
||||
'packing.clearCheckedShort': 'Supprimer {count}',
|
||||
@@ -1137,6 +1169,19 @@ const fr: Record<string, string> = {
|
||||
'memories.oldest': 'Plus anciennes',
|
||||
'memories.newest': 'Plus récentes',
|
||||
'memories.allLocations': 'Tous les lieux',
|
||||
'memories.addPhotos': 'Ajouter des photos',
|
||||
'memories.selectPhotos': 'Sélectionner des photos depuis Immich',
|
||||
'memories.selectHint': 'Appuyez sur les photos pour les sélectionner.',
|
||||
'memories.selected': 'sélectionné(s)',
|
||||
'memories.addSelected': 'Ajouter {count} photos',
|
||||
'memories.alreadyAdded': 'Ajouté',
|
||||
'memories.private': 'Privé',
|
||||
'memories.stopSharing': 'Arrêter le partage',
|
||||
'memories.tripDates': 'Dates du voyage',
|
||||
'memories.allPhotos': 'Toutes les photos',
|
||||
'memories.confirmShareTitle': 'Partager avec les membres du voyage ?',
|
||||
'memories.confirmShareHint': '{count} photos seront visibles par tous les membres de ce voyage. Vous pourrez rendre des photos individuelles privées plus tard.',
|
||||
'memories.confirmShareButton': 'Partager les photos',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'Chat',
|
||||
|
||||
@@ -139,6 +139,7 @@ const nl: Record<string, string> = {
|
||||
'settings.temperature': 'Temperatuureenheid',
|
||||
'settings.timeFormat': 'Tijdnotatie',
|
||||
'settings.routeCalculation': 'Routeberekening',
|
||||
'settings.blurBookingCodes': 'Boekingscodes vervagen',
|
||||
'settings.on': 'Aan',
|
||||
'settings.off': 'Uit',
|
||||
'settings.account': 'Account',
|
||||
@@ -475,6 +476,14 @@ const nl: Record<string, string> = {
|
||||
'vacay.carriedOver': 'van {year}',
|
||||
'vacay.blockWeekends': 'Weekenden blokkeren',
|
||||
'vacay.blockWeekendsHint': 'Voorkom vakantie-invoeren op zaterdag en zondag',
|
||||
'vacay.weekendDays': 'Weekenddagen',
|
||||
'vacay.mon': 'Ma',
|
||||
'vacay.tue': 'Di',
|
||||
'vacay.wed': 'Wo',
|
||||
'vacay.thu': 'Do',
|
||||
'vacay.fri': 'Vr',
|
||||
'vacay.sat': 'Za',
|
||||
'vacay.sun': 'Zo',
|
||||
'vacay.publicHolidays': 'Feestdagen',
|
||||
'vacay.publicHolidaysHint': 'Markeer feestdagen in de kalender',
|
||||
'vacay.selectCountry': 'Selecteer land',
|
||||
@@ -618,9 +627,18 @@ const nl: Record<string, string> = {
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'Dagplan exporteren als PDF',
|
||||
'dayplan.pdfError': 'PDF-export mislukt',
|
||||
'dayplan.cannotReorderTransport': 'Boekingen met een vast tijdstip kunnen niet worden verplaatst',
|
||||
'dayplan.confirmRemoveTimeTitle': 'Tijd verwijderen?',
|
||||
'dayplan.confirmRemoveTimeBody': 'Deze plek heeft een vast tijdstip ({time}). Verplaatsen verwijdert het tijdstip en maakt vrije sortering mogelijk.',
|
||||
'dayplan.confirmRemoveTimeAction': 'Tijd verwijderen en verplaatsen',
|
||||
'dayplan.cannotDropOnTimed': 'Items kunnen niet tussen tijdgebonden items worden geplaatst',
|
||||
'dayplan.cannotBreakChronology': 'Dit zou de chronologische volgorde van geplande items en boekingen doorbreken',
|
||||
|
||||
// Places Sidebar
|
||||
'places.addPlace': 'Plaats/activiteit toevoegen',
|
||||
'places.importGpx': 'GPX importeren',
|
||||
'places.gpxImported': '{count} plaatsen geïmporteerd uit GPX',
|
||||
'places.gpxError': 'GPX-import mislukt',
|
||||
'places.assignToDay': 'Aan welke dag toevoegen?',
|
||||
'places.all': 'Alle',
|
||||
'places.unplanned': 'Ongepland',
|
||||
@@ -724,6 +742,8 @@ const nl: Record<string, string> = {
|
||||
'reservations.type.tour': 'Rondleiding',
|
||||
'reservations.type.other': 'Overig',
|
||||
'reservations.confirm.delete': 'Weet je zeker dat je de reservering "{name}" wilt verwijderen?',
|
||||
'reservations.confirm.deleteTitle': 'Boeking verwijderen?',
|
||||
'reservations.confirm.deleteBody': '"{name}" wordt permanent verwijderd.',
|
||||
'reservations.toast.updated': 'Reservering bijgewerkt',
|
||||
'reservations.toast.removed': 'Reservering verwijderd',
|
||||
'reservations.toast.fileUploaded': 'Bestand geüpload',
|
||||
@@ -781,6 +801,9 @@ const nl: Record<string, string> = {
|
||||
'budget.paid': 'Betaald',
|
||||
'budget.open': 'Open',
|
||||
'budget.noMembers': 'Geen leden toegewezen',
|
||||
'budget.settlement': 'Afrekening',
|
||||
'budget.settlementInfo': 'Klik op de avatar van een lid bij een budgetpost om deze groen te markeren — dit betekent dat diegene heeft betaald. De afrekening toont vervolgens wie wie hoeveel verschuldigd is.',
|
||||
'budget.netBalances': 'Nettosaldi',
|
||||
|
||||
// Files
|
||||
'files.title': 'Bestanden',
|
||||
@@ -834,6 +857,15 @@ const nl: Record<string, string> = {
|
||||
// Packing
|
||||
'packing.title': 'Paklijst',
|
||||
'packing.empty': 'Paklijst is leeg',
|
||||
'packing.import': 'Importeren',
|
||||
'packing.importTitle': 'Paklijst importeren',
|
||||
'packing.importHint': 'Eén item per regel. Optioneel categorie en aantal gescheiden door komma, puntkomma of tab: Naam, Categorie, Aantal',
|
||||
'packing.importPlaceholder': 'Tandenborstel\nZonnebrand, Hygiëne\nT-Shirts, Kleding, 5\nPaspoort, Documenten',
|
||||
'packing.importCsv': 'CSV/TXT laden',
|
||||
'packing.importAction': '{count} importeren',
|
||||
'packing.importSuccess': '{count} items geïmporteerd',
|
||||
'packing.importError': 'Import mislukt',
|
||||
'packing.importEmpty': 'Geen items om te importeren',
|
||||
'packing.progress': '{packed} van {total} ingepakt ({percent}%)',
|
||||
'packing.clearChecked': '{count} aangevinkte verwijderen',
|
||||
'packing.clearCheckedShort': '{count} verwijderen',
|
||||
@@ -1137,6 +1169,19 @@ const nl: Record<string, string> = {
|
||||
'memories.oldest': 'Oudste eerst',
|
||||
'memories.newest': 'Nieuwste eerst',
|
||||
'memories.allLocations': 'Alle locaties',
|
||||
'memories.addPhotos': 'Foto\'s toevoegen',
|
||||
'memories.selectPhotos': 'Selecteer foto\'s uit Immich',
|
||||
'memories.selectHint': 'Tik op foto\'s om ze te selecteren.',
|
||||
'memories.selected': 'geselecteerd',
|
||||
'memories.addSelected': '{count} foto\'s toevoegen',
|
||||
'memories.alreadyAdded': 'Toegevoegd',
|
||||
'memories.private': 'Privé',
|
||||
'memories.stopSharing': 'Delen stoppen',
|
||||
'memories.tripDates': 'Reisdata',
|
||||
'memories.allPhotos': 'Alle foto\'s',
|
||||
'memories.confirmShareTitle': 'Delen met reisgenoten?',
|
||||
'memories.confirmShareHint': '{count} foto\'s worden zichtbaar voor alle leden van deze reis. Je kunt individuele foto\'s later privé maken.',
|
||||
'memories.confirmShareButton': 'Foto\'s delen',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'Chat',
|
||||
|
||||
@@ -139,6 +139,7 @@ const ru: Record<string, string> = {
|
||||
'settings.temperature': 'Единица температуры',
|
||||
'settings.timeFormat': 'Формат времени',
|
||||
'settings.routeCalculation': 'Расчёт маршрута',
|
||||
'settings.blurBookingCodes': 'Скрыть коды бронирования',
|
||||
'settings.on': 'Вкл.',
|
||||
'settings.off': 'Выкл.',
|
||||
'settings.account': 'Аккаунт',
|
||||
@@ -475,6 +476,14 @@ const ru: Record<string, string> = {
|
||||
'vacay.carriedOver': 'из {year}',
|
||||
'vacay.blockWeekends': 'Блокировать выходные',
|
||||
'vacay.blockWeekendsHint': 'Запретить записи об отпуске в субботу и воскресенье',
|
||||
'vacay.weekendDays': 'Выходные дни',
|
||||
'vacay.mon': 'Пн',
|
||||
'vacay.tue': 'Вт',
|
||||
'vacay.wed': 'Ср',
|
||||
'vacay.thu': 'Чт',
|
||||
'vacay.fri': 'Пт',
|
||||
'vacay.sat': 'Сб',
|
||||
'vacay.sun': 'Вс',
|
||||
'vacay.publicHolidays': 'Государственные праздники',
|
||||
'vacay.publicHolidaysHint': 'Отмечать государственные праздники в календаре',
|
||||
'vacay.selectCountry': 'Выберите страну',
|
||||
@@ -618,9 +627,18 @@ const ru: Record<string, string> = {
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'Экспортировать план дня в PDF',
|
||||
'dayplan.pdfError': 'Ошибка экспорта PDF',
|
||||
'dayplan.cannotReorderTransport': 'Бронирования с фиксированным временем нельзя перемещать',
|
||||
'dayplan.confirmRemoveTimeTitle': 'Удалить время?',
|
||||
'dayplan.confirmRemoveTimeBody': 'У этого места фиксированное время ({time}). При перемещении время будет удалено, и станет доступна свободная сортировка.',
|
||||
'dayplan.confirmRemoveTimeAction': 'Удалить время и переместить',
|
||||
'dayplan.cannotDropOnTimed': 'Элементы нельзя размещать между записями с фиксированным временем',
|
||||
'dayplan.cannotBreakChronology': 'Это нарушит хронологический порядок запланированных элементов и бронирований',
|
||||
|
||||
// Places Sidebar
|
||||
'places.addPlace': 'Добавить место/активность',
|
||||
'places.importGpx': 'Импорт GPX',
|
||||
'places.gpxImported': '{count} мест импортировано из GPX',
|
||||
'places.gpxError': 'Ошибка импорта GPX',
|
||||
'places.assignToDay': 'Добавить в какой день?',
|
||||
'places.all': 'Все',
|
||||
'places.unplanned': 'Незапланированные',
|
||||
@@ -724,6 +742,8 @@ const ru: Record<string, string> = {
|
||||
'reservations.type.tour': 'Экскурсия',
|
||||
'reservations.type.other': 'Другое',
|
||||
'reservations.confirm.delete': 'Вы уверены, что хотите удалить бронирование «{name}»?',
|
||||
'reservations.confirm.deleteTitle': 'Удалить бронирование?',
|
||||
'reservations.confirm.deleteBody': '«{name}» будет удалено навсегда.',
|
||||
'reservations.toast.updated': 'Бронирование обновлено',
|
||||
'reservations.toast.removed': 'Бронирование удалено',
|
||||
'reservations.toast.fileUploaded': 'Файл загружен',
|
||||
@@ -781,6 +801,9 @@ const ru: Record<string, string> = {
|
||||
'budget.paid': 'Оплачено',
|
||||
'budget.open': 'Не оплачено',
|
||||
'budget.noMembers': 'Участники не назначены',
|
||||
'budget.settlement': 'Взаиморасчёт',
|
||||
'budget.settlementInfo': 'Нажмите на аватар участника в строке бюджета, чтобы отметить его зелёным — это значит, что он заплатил. Взаиморасчёт покажет, кто кому и сколько должен.',
|
||||
'budget.netBalances': 'Чистые балансы',
|
||||
|
||||
// Files
|
||||
'files.title': 'Файлы',
|
||||
@@ -834,6 +857,15 @@ const ru: Record<string, string> = {
|
||||
// Packing
|
||||
'packing.title': 'Список вещей',
|
||||
'packing.empty': 'Список вещей пуст',
|
||||
'packing.import': 'Импорт',
|
||||
'packing.importTitle': 'Импорт списка вещей',
|
||||
'packing.importHint': 'Один предмет на строку. Категория и количество — через запятую, точку с запятой или табуляцию: Название, Категория, Количество',
|
||||
'packing.importPlaceholder': 'Зубная щётка\nСолнцезащитный крем, Гигиена\nФутболки, Одежда, 5\nПаспорт, Документы',
|
||||
'packing.importCsv': 'Загрузить CSV/TXT',
|
||||
'packing.importAction': 'Импортировать {count}',
|
||||
'packing.importSuccess': '{count} предметов импортировано',
|
||||
'packing.importError': 'Ошибка импорта',
|
||||
'packing.importEmpty': 'Нет предметов для импорта',
|
||||
'packing.progress': '{packed} из {total} собрано ({percent}%)',
|
||||
'packing.clearChecked': 'Удалить {count} отмеченных',
|
||||
'packing.clearCheckedShort': 'Удалить {count}',
|
||||
@@ -1137,6 +1169,19 @@ const ru: Record<string, string> = {
|
||||
'memories.oldest': 'Сначала старые',
|
||||
'memories.newest': 'Сначала новые',
|
||||
'memories.allLocations': 'Все места',
|
||||
'memories.addPhotos': 'Добавить фото',
|
||||
'memories.selectPhotos': 'Выбрать фото из Immich',
|
||||
'memories.selectHint': 'Нажмите на фото, чтобы выбрать их.',
|
||||
'memories.selected': 'выбрано',
|
||||
'memories.addSelected': 'Добавить {count} фото',
|
||||
'memories.alreadyAdded': 'Добавлено',
|
||||
'memories.private': 'Приватное',
|
||||
'memories.stopSharing': 'Прекратить доступ',
|
||||
'memories.tripDates': 'Даты поездки',
|
||||
'memories.allPhotos': 'Все фото',
|
||||
'memories.confirmShareTitle': 'Поделиться с участниками поездки?',
|
||||
'memories.confirmShareHint': '{count} фото станут видны всем участникам этой поездки. Вы сможете сделать отдельные фото приватными позже.',
|
||||
'memories.confirmShareButton': 'Поделиться фото',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'Чат',
|
||||
|
||||
@@ -139,6 +139,7 @@ const zh: Record<string, string> = {
|
||||
'settings.temperature': '温度单位',
|
||||
'settings.timeFormat': '时间格式',
|
||||
'settings.routeCalculation': '路线计算',
|
||||
'settings.blurBookingCodes': '模糊预订代码',
|
||||
'settings.on': '开',
|
||||
'settings.off': '关',
|
||||
'settings.account': '账户',
|
||||
@@ -475,6 +476,14 @@ const zh: Record<string, string> = {
|
||||
'vacay.carriedOver': '从 {year} 结转',
|
||||
'vacay.blockWeekends': '锁定周末',
|
||||
'vacay.blockWeekendsHint': '禁止在周六和周日安排假期',
|
||||
'vacay.weekendDays': '周末',
|
||||
'vacay.mon': '周一',
|
||||
'vacay.tue': '周二',
|
||||
'vacay.wed': '周三',
|
||||
'vacay.thu': '周四',
|
||||
'vacay.fri': '周五',
|
||||
'vacay.sat': '周六',
|
||||
'vacay.sun': '周日',
|
||||
'vacay.publicHolidays': '公共假日',
|
||||
'vacay.publicHolidaysHint': '在日历中标记公共假日',
|
||||
'vacay.selectCountry': '选择国家',
|
||||
@@ -618,9 +627,18 @@ const zh: Record<string, string> = {
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': '导出当天计划为 PDF',
|
||||
'dayplan.pdfError': 'PDF 导出失败',
|
||||
'dayplan.cannotReorderTransport': '有固定时间的预订无法重新排序',
|
||||
'dayplan.confirmRemoveTimeTitle': '移除时间?',
|
||||
'dayplan.confirmRemoveTimeBody': '此地点有固定时间({time})。移动后将移除时间并允许自由排序。',
|
||||
'dayplan.confirmRemoveTimeAction': '移除时间并移动',
|
||||
'dayplan.cannotDropOnTimed': '无法将项目放置在有固定时间的条目之间',
|
||||
'dayplan.cannotBreakChronology': '这将打乱已计划项目和预订的时间顺序',
|
||||
|
||||
// Places Sidebar
|
||||
'places.addPlace': '添加地点/活动',
|
||||
'places.importGpx': '导入 GPX',
|
||||
'places.gpxImported': '已从 GPX 导入 {count} 个地点',
|
||||
'places.gpxError': 'GPX 导入失败',
|
||||
'places.assignToDay': '添加到哪一天?',
|
||||
'places.all': '全部',
|
||||
'places.unplanned': '未规划',
|
||||
@@ -724,6 +742,8 @@ const zh: Record<string, string> = {
|
||||
'reservations.type.tour': '旅游团',
|
||||
'reservations.type.other': '其他',
|
||||
'reservations.confirm.delete': '确定要删除预订「{name}」吗?',
|
||||
'reservations.confirm.deleteTitle': '删除预订?',
|
||||
'reservations.confirm.deleteBody': '"{name}" 将被永久删除。',
|
||||
'reservations.toast.updated': '预订已更新',
|
||||
'reservations.toast.removed': '预订已删除',
|
||||
'reservations.toast.fileUploaded': '文件已上传',
|
||||
@@ -781,6 +801,9 @@ const zh: Record<string, string> = {
|
||||
'budget.paid': '已支付',
|
||||
'budget.open': '未支付',
|
||||
'budget.noMembers': '未分配成员',
|
||||
'budget.settlement': '结算',
|
||||
'budget.settlementInfo': '点击预算项目上的成员头像将其标记为绿色——表示该成员已付款。结算会显示谁欠谁多少。',
|
||||
'budget.netBalances': '净余额',
|
||||
|
||||
// Files
|
||||
'files.title': '文件',
|
||||
@@ -834,6 +857,15 @@ const zh: Record<string, string> = {
|
||||
// Packing
|
||||
'packing.title': '行李清单',
|
||||
'packing.empty': '行李清单为空',
|
||||
'packing.import': '导入',
|
||||
'packing.importTitle': '导入装箱清单',
|
||||
'packing.importHint': '每行一个物品。可选用逗号、分号或制表符分隔类别和数量:名称, 类别, 数量',
|
||||
'packing.importPlaceholder': '牙刷\n防晒霜, 卫生\nT恤, 衣物, 5\n护照, 证件',
|
||||
'packing.importCsv': '加载 CSV/TXT',
|
||||
'packing.importAction': '导入 {count}',
|
||||
'packing.importSuccess': '已导入 {count} 项',
|
||||
'packing.importError': '导入失败',
|
||||
'packing.importEmpty': '没有可导入的项目',
|
||||
'packing.progress': '已打包 {packed}/{total}({percent}%)',
|
||||
'packing.clearChecked': '移除 {count} 个已勾选',
|
||||
'packing.clearCheckedShort': '移除 {count} 个',
|
||||
@@ -1137,6 +1169,19 @@ const zh: Record<string, string> = {
|
||||
'memories.oldest': '最早优先',
|
||||
'memories.newest': '最新优先',
|
||||
'memories.allLocations': '所有地点',
|
||||
'memories.addPhotos': '添加照片',
|
||||
'memories.selectPhotos': '从 Immich 选择照片',
|
||||
'memories.selectHint': '点击照片以选择。',
|
||||
'memories.selected': '已选择',
|
||||
'memories.addSelected': '添加 {count} 张照片',
|
||||
'memories.alreadyAdded': '已添加',
|
||||
'memories.private': '私密',
|
||||
'memories.stopSharing': '停止分享',
|
||||
'memories.tripDates': '旅行日期',
|
||||
'memories.allPhotos': '所有照片',
|
||||
'memories.confirmShareTitle': '与旅行成员分享?',
|
||||
'memories.confirmShareHint': '{count} 张照片将对本次旅行的所有成员可见。你可以稍后将单张照片设为私密。',
|
||||
'memories.confirmShareButton': '分享照片',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': '聊天',
|
||||
|
||||
Reference in New Issue
Block a user