fix(i18n): translate hardcoded strings in JourneyDetailPage and fix ellipsis in all languages

- Replace all remaining hardcoded strings in JourneyDetailPage JourneySettingsDialog with t() calls
- Add 14 missing translation keys to all 13 non-English language files
  (trips.member*, common.expand/collapse, inspector.remove, memories.*, journey.*)
- Fix common.loading and common.saving to use Unicode ellipsis (…) instead of three dots (...)
- Update 4 test files that expected three-dot ellipsis to use Unicode ellipsis
- All 2541 tests passing
This commit is contained in:
Isaias Tavares
2026-04-12 17:29:11 -03:00
parent 0fe1c443e9
commit af789b7f7c
24 changed files with 248 additions and 70 deletions
@@ -47,7 +47,7 @@ describe('AuditLogPanel', () => {
}),
);
render(<AuditLogPanel serverTimezone="UTC" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.getByText('Loading')).toBeInTheDocument();
expect(document.querySelector('table')).not.toBeInTheDocument();
});
@@ -55,7 +55,7 @@ describe('GitHubPanel', () => {
it('FE-ADMIN-GH-002: all support links have correct href and target=_blank', async () => {
render(<GitHubPanel />);
await waitFor(() => expect(screen.queryByText('Loading...')).not.toBeInTheDocument());
await waitFor(() => expect(screen.queryByText('Loading')).not.toBeInTheDocument());
const kofi = screen.getByText('Ko-fi').closest('a')!;
expect(kofi).toHaveAttribute('href', 'https://ko-fi.com/mauriceboe');
@@ -272,7 +272,7 @@ describe('GitHubPanel', () => {
it('FE-ADMIN-GH-016: support card hover effects fire without error', async () => {
render(<GitHubPanel />);
await waitFor(() => expect(screen.queryByText('Loading...')).not.toBeInTheDocument());
await waitFor(() => expect(screen.queryByText('Loading')).not.toBeInTheDocument());
const kofiLink = screen.getByText('Ko-fi').closest('a')!;
fireEvent.mouseEnter(kofiLink);
@@ -189,7 +189,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
</div>
{!collapsed && formattedDate && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 1 }}>{formattedDate}</div>}
</div>
<button onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? 'Expand' : 'Collapse'}
<button onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? t('common.expand') : t('common.collapse')}
style={{ background: 'var(--bg-secondary)', border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s ease' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}>
@@ -601,7 +601,7 @@ export default function PlaceInspector({
{selectedDayId && (
assignmentInDay ? (
<ActionButton onClick={() => onRemoveAssignment(selectedDayId, assignmentInDay.id)} variant="ghost" icon={<Minus size={13} />}
label={<><span className="hidden sm:inline">{t('inspector.removeFromDay')}</span><span className="sm:hidden">Remove</span></>} />
label={<><span className="hidden sm:inline">{t('inspector.removeFromDay')}</span><span className="sm:hidden">{t('inspector.remove')}</span></>} />
) : (
<ActionButton onClick={() => onAssignToDay(place.id)} variant="primary" icon={<Plus size={13} />} label={t('inspector.addToDay')} />
)
@@ -107,7 +107,7 @@ export default function NotificationsTab(): React.ReactElement {
}
const renderContent = () => {
if (!matrix) return <p style={{ fontSize: 12, color: 'var(--text-faint)', fontStyle: 'italic' }}>Loading</p>
if (!matrix) return <p style={{ fontSize: 12, color: 'var(--text-faint)', fontStyle: 'italic' }}>{t('common.loading')}</p>
if (visibleChannels.length === 0) {
return (
@@ -119,7 +119,7 @@ export default function NotificationsTab(): React.ReactElement {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{saving && <p style={{ fontSize: 11, color: 'var(--text-faint)', marginBottom: 8 }}>Saving</p>}
{saving && <p style={{ fontSize: 11, color: 'var(--text-faint)', marginBottom: 8 }}>{t('common.saving')}</p>}
{matrix.available_channels.webhook && (
<div style={{ marginBottom: 16, padding: '12px', background: 'var(--bg-secondary)', borderRadius: 8, border: '1px solid var(--border-primary)' }}>
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 4 }}>
@@ -253,7 +253,7 @@ export default function PhotoProvidersSection(): React.ReactElement {
onClick={() => handleSaveProvider(provider)}
disabled={!canSave || !!saving[provider.id] || isProviderSaveDisabled(provider)}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:bg-slate-400"
title={!canSave ? 'Save route is not configured for this provider' : isProviderSaveDisabled(provider) ? 'Please fill all required fields' : ''}
title={!canSave ? t('memories.saveRouteNotConfigured') : isProviderSaveDisabled(provider) ? t('memories.fillRequiredFields') : ''}
>
<Save className="w-4 h-4" /> {t('common.save')}
</button>
@@ -261,7 +261,7 @@ export default function PhotoProvidersSection(): React.ReactElement {
onClick={() => handleTestProvider(provider)}
disabled={!canTest || testing}
className="flex items-center gap-2 px-4 py-2 border border-slate-200 rounded-lg text-sm hover:bg-slate-50"
title={!canTest ? 'Test route is not configured for this provider' : ''}
title={!canTest ? t('memories.testRouteNotConfigured') : ''}
>
{testing
? <div className="w-4 h-4 border-2 border-slate-300 border-t-slate-700 rounded-full animate-spin" />
@@ -284,6 +284,6 @@ describe('TripFormModal', () => {
const submitBtns = screen.getAllByText('Create New Trip');
const submitBtn = submitBtns.find(el => el.closest('button'))!;
await user.click(submitBtn.closest('button')!);
await waitFor(() => expect(screen.getByText('Saving...')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Saving')).toBeInTheDocument());
});
});
@@ -385,8 +385,8 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
try {
await tripsApi.removeMember(trip!.id, m.id)
setExistingMembers(prev => prev.filter(x => x.id !== m.id))
toast.success(`${m.username} removed`)
} catch { toast.error('Failed to remove') }
toast.success(t('trips.memberRemoved', { username: m.username }))
} catch { toast.error(t('trips.memberRemoveError')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 99,
@@ -431,8 +431,8 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
try {
await tripsApi.addMember(trip.id, user.username)
setExistingMembers(prev => [...prev, { id: user.id, username: user.username }])
toast.success(`${user.username} added`)
} catch { toast.error('Failed to add') }
toast.success(t('trips.memberAdded', { username: user.username }))
} catch { toast.error(t('trips.memberAddError')) }
}
} else {
setSelectedMembers(prev => prev.includes(Number(value)) ? prev : [...prev, Number(value)])
+5 -3
View File
@@ -1,6 +1,7 @@
import { useState, useRef } from 'react'
import { useTripStore } from '../store/tripStore'
import { useToast } from '../components/shared/Toast'
import { useTranslation } from '../i18n'
import type { MergedItem, DayNotesMap, DayNote } from '../types'
interface NoteUiState {
@@ -21,6 +22,7 @@ export function useDayNotes(tripId: number | string) {
const noteInputRef = useRef<HTMLInputElement | null>(null)
const tripStore = useTripStore()
const toast = useToast()
const { t } = useTranslation()
const dayNotes: DayNotesMap = tripStore.dayNotes || {}
const openAddNote = (dayId: number, getMergedItems: (dayId: number) => MergedItem[], expandDay?: (dayId: number) => void) => {
@@ -50,12 +52,12 @@ export function useDayNotes(tripId: number | string) {
await tripStore.updateDayNote(tripId, dayId, ui.noteId!, { text: ui.text.trim(), time: ui.time || null, icon: ui.icon || 'FileText' })
}
cancelNote(dayId)
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}
const deleteNote = async (dayId: number, noteId: number) => {
try { await tripStore.deleteDayNote(tripId, dayId, noteId) }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}
const moveNote = async (dayId: number, noteId: number, direction: 'up' | 'down', getMergedItems: (dayId: number) => MergedItem[]) => {
@@ -71,7 +73,7 @@ export function useDayNotes(tripId: number | string) {
newSortOrder = idx < merged.length - 2 ? (merged[idx + 1].sortKey + merged[idx + 2].sortKey) / 2 : merged[idx + 1].sortKey + 1
}
try { await tripStore.updateDayNote(tripId, dayId, noteId, { sort_order: newSortOrder }) }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}
return { noteUi, setNoteUi, noteInputRef, dayNotes, openAddNote, openEditNote, cancelNote, saveNote, deleteNote, moveNote }
+14 -1
View File
@@ -33,6 +33,12 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'common.password': 'كلمة المرور',
'common.saving': 'جارٍ الحفظ...',
'common.saved': 'تم الحفظ',
'common.expand': 'توسيع',
'common.collapse': 'طي',
'trips.memberRemoved': '{username} تمت إزالته',
'trips.memberRemoveError': 'فشل في الإزالة',
'trips.memberAdded': '{username} تمت إضافته',
'trips.memberAddError': 'فشل في الإضافة',
'trips.reminder': 'تذكير',
'trips.reminderNone': 'بدون',
'trips.reminderDay': 'يوم',
@@ -940,6 +946,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'inspector.files': 'الملفات',
'inspector.filesCount': '{count} ملفات',
'inspector.removeFromDay': 'إزالة من اليوم',
'inspector.remove': 'إزالة',
'inspector.addToDay': 'إضافة إلى اليوم',
'inspector.confirmedRes': 'حجز مؤكد',
'inspector.pendingRes': 'حجز قيد الانتظار',
@@ -1175,7 +1182,6 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'تحديد الكل',
'packing.menuUncheckAll': 'إلغاء تحديد الكل',
'packing.menuDeleteCat': 'حذف الفئة',
'packing.assignUser': 'تعيين مستخدم',
'packing.noMembers': 'لا أعضاء',
'packing.addItem': 'إضافة عنصر',
'packing.addItemPlaceholder': 'اسم العنصر...',
@@ -1502,6 +1508,9 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'memories.saved': 'تم حفظ إعدادات {provider_name}',
'memories.providerDisconnectedBanner': 'اتصالك بـ {provider_name} مفقود. أعد الاتصال في الإعدادات لعرض الصور.',
'memories.saveError': 'تعذّر حفظ إعدادات {provider_name}',
'memories.saveRouteNotConfigured': 'مسار الحفظ غير مهيأ لهذا المزود',
'memories.testRouteNotConfigured': 'مسار الاختبار غير مهيأ لهذا المزود',
'memories.fillRequiredFields': 'يرجى ملء جميع الحقول المطلوبة',
'memories.oldest': 'الأقدم أولاً',
'memories.newest': 'الأحدث أولاً',
'memories.allLocations': 'جميع المواقع',
@@ -1526,6 +1535,10 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟',
'memories.confirmShareHint': '{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
'memories.confirmShareButton': 'مشاركة الصور',
'journey.settings.failedToDelete': 'فشل في الحذف',
'journey.entries.deleteTitle': 'حذف الإدخال',
'journey.photosUploaded': 'تم رفع {count} صورة',
'journey.photosAdded': 'تمت إضافة {count} صورة',
// Collab Addon
'collab.tabs.chat': 'الدردشة',
+14 -1
View File
@@ -29,11 +29,17 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'common.password': 'Senha',
'common.saving': 'Salvando...',
'common.saved': 'Salvo',
'common.expand': 'Expandir',
'common.collapse': 'Recolher',
'trips.reminder': 'Lembrete',
'trips.reminderNone': 'Nenhum',
'trips.reminderDay': 'dia',
'trips.reminderDays': 'dias',
'trips.reminderCustom': 'Personalizado',
'trips.memberRemoved': '{username} removido',
'trips.memberRemoveError': 'Falha ao remover',
'trips.memberAdded': '{username} adicionado',
'trips.memberAddError': 'Falha ao adicionar',
'trips.reminderDaysBefore': 'dias antes da partida',
'trips.reminderDisabledHint': 'Os lembretes de viagem estão desativados. Ative-os em Admin > Configurações > Notificações.',
'common.update': 'Atualizar',
@@ -909,6 +915,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'inspector.files': 'Arquivos',
'inspector.filesCount': '{count} arquivos',
'inspector.removeFromDay': 'Remover do dia',
'inspector.remove': 'Remover',
'inspector.addToDay': 'Adicionar ao dia',
'inspector.confirmedRes': 'Reserva confirmada',
'inspector.pendingRes': 'Reserva pendente',
@@ -1134,7 +1141,6 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'packing.allPacked': 'Tudo na mala!',
'packing.addPlaceholder': 'Adicionar item...',
'packing.categoryPlaceholder': 'Categoria...',
'packing.assignUser': 'Atribuir usuário',
'packing.saveAsTemplate': 'Salvar como modelo',
'packing.templateName': 'Nome do modelo',
'packing.templateSaved': 'Lista de bagagem salva como modelo',
@@ -1808,6 +1814,9 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'memories.providerUsername': 'Nome de usuário',
'memories.providerPassword': 'Senha',
'memories.saveError': 'Não foi possível salvar as configurações de {provider_name}',
'memories.saveRouteNotConfigured': 'A rota de salvamento não está configurada para este provedor',
'memories.testRouteNotConfigured': 'A rota de teste não está configurada para este provedor',
'memories.fillRequiredFields': 'Por favor preencha todos os campos obrigatórios',
'memories.selectAlbumMultiple': 'Selecionar álbum',
'memories.selectPhotosMultiple': 'Selecionar fotos',
'journey.title': 'Jornada',
@@ -1977,6 +1986,10 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'journey.settings.saveFailed': 'Não foi possível salvar',
'journey.settings.coverUpdated': 'Capa atualizada',
'journey.settings.coverFailed': 'Falha no envio',
'journey.settings.failedToDelete': 'Falha ao excluir',
'journey.entries.deleteTitle': 'Excluir entrada',
'journey.photosUploaded': '{count} fotos enviadas',
'journey.photosAdded': '{count} fotos adicionadas',
'journey.public.notFound': 'Não encontrado',
'journey.public.notFoundMessage': 'Esta jornada não existe ou o link expirou.',
'journey.public.readOnly': 'Somente leitura · Jornada pública',
+14 -1
View File
@@ -28,6 +28,12 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'common.email': 'E-mail',
'common.password': 'Heslo',
'common.saving': 'Ukládání...',
'trips.memberRemoved': '{username} odebrán',
'trips.memberRemoveError': 'Odebrání se nezdařilo',
'trips.memberAdded': '{username} přidán',
'trips.memberAddError': 'Přidání se nezdařilo',
'common.expand': 'Rozbalit',
'common.collapse': 'Sbalit',
'common.saved': 'Uloženo',
'trips.reminder': 'Připomínka',
'trips.reminderNone': 'Žádná',
@@ -938,6 +944,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'inspector.files': 'Soubory',
'inspector.filesCount': '{count} souborů',
'inspector.removeFromDay': 'Odebrat ze dne',
'inspector.remove': 'Odstranit',
'inspector.addToDay': 'Přidat ke dni',
'inspector.confirmedRes': 'Potvrzená rezervace',
'inspector.pendingRes': 'Čekající rezervace',
@@ -1173,7 +1180,6 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'Označit vše',
'packing.menuUncheckAll': 'Odznačit vše',
'packing.menuDeleteCat': 'Smazat kategorii',
'packing.assignUser': 'Přiřadit uživatele',
'packing.noMembers': 'Žádní členové cesty',
'packing.addItem': 'Přidat položku',
'packing.addItemPlaceholder': 'Název položky...',
@@ -1810,6 +1816,9 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'memories.providerUsername': 'Uživatelské jméno',
'memories.providerPassword': 'Heslo',
'memories.saveError': 'Nepodařilo se uložit nastavení {provider_name}',
'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele',
'memories.testRouteNotConfigured': 'Testovací trasa není nakonfigurována pro tohoto poskytovatele',
'memories.fillRequiredFields': 'Prosím vyplňte všechna povinná pole',
'memories.selectAlbumMultiple': 'Vybrat album',
'memories.selectPhotosMultiple': 'Vybrat fotky',
'journey.title': 'Cestovní deník',
@@ -1979,6 +1988,10 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'journey.settings.saveFailed': 'Uložení selhalo',
'journey.settings.coverUpdated': 'Obal aktualizován',
'journey.settings.coverFailed': 'Nahrávání selhalo',
'journey.settings.failedToDelete': 'Smazání se nezdařilo',
'journey.entries.deleteTitle': 'Smazat záznam',
'journey.photosUploaded': '{count} fotografií nahráno',
'journey.photosAdded': '{count} fotografií přidáno',
'journey.public.notFound': 'Nenalezeno',
'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.',
'journey.public.readOnly': 'Pouze ke čtení · Veřejný cestovní deník',
+14 -1
View File
@@ -28,6 +28,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'common.email': 'E-Mail',
'common.password': 'Passwort',
'common.saving': 'Speichern...',
'common.expand': 'Erweitern',
'common.collapse': 'Einklappen',
'common.justNow': 'gerade eben',
'common.hoursAgo': 'vor {count}h',
'common.daysAgo': 'vor {count}T',
@@ -37,6 +39,10 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'trips.reminderDay': 'Tag',
'trips.reminderDays': 'Tage',
'trips.reminderCustom': 'Benutzerdefiniert',
'trips.memberRemoved': '{username} entfernt',
'trips.memberRemoveError': 'Entfernen fehlgeschlagen',
'trips.memberAdded': '{username} hinzugefügt',
'trips.memberAddError': 'Hinzufügen fehlgeschlagen',
'trips.reminderDaysBefore': 'Tage vor Abreise',
'trips.reminderDisabledHint': 'Reiseerinnerungen sind deaktiviert. Aktivieren Sie sie unter Admin > Einstellungen > Benachrichtigungen.',
'common.update': 'Aktualisieren',
@@ -940,6 +946,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'inspector.files': 'Dateien',
'inspector.filesCount': '{count} Dateien',
'inspector.removeFromDay': 'Vom Tag entfernen',
'inspector.remove': 'Entfernen',
'inspector.addToDay': 'Zum Tag hinzufügen',
'inspector.confirmedRes': 'Bestätigte Reservierung',
'inspector.pendingRes': 'Ausstehende Reservierung',
@@ -1174,7 +1181,6 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'Alle abhaken',
'packing.menuUncheckAll': 'Alle Haken entfernen',
'packing.menuDeleteCat': 'Kategorie löschen',
'packing.assignUser': 'Benutzer zuweisen',
'packing.noMembers': 'Keine Mitglieder',
'packing.addItem': 'Eintrag hinzufügen',
'packing.addItemPlaceholder': 'Artikelname...',
@@ -1501,6 +1507,9 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'memories.saved': '{provider_name}-Einstellungen gespeichert',
'memories.providerDisconnectedBanner': 'Deine {provider_name}-Verbindung wurde getrennt. Verbinde erneut in den Einstellungen, um Fotos anzuzeigen.',
'memories.saveError': '{provider_name}-Einstellungen konnten nicht gespeichert werden',
'memories.saveRouteNotConfigured': 'Speicherroute ist für diesen Anbieter nicht konfiguriert',
'memories.testRouteNotConfigured': 'Testroute ist für diesen Anbieter nicht konfiguriert',
'memories.fillRequiredFields': 'Bitte füllen Sie alle Pflichtfelder aus',
'memories.addPhotos': 'Fotos hinzufügen',
'memories.linkAlbum': 'Album verknüpfen',
'memories.selectAlbum': 'Immich-Album auswählen',
@@ -1967,6 +1976,10 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'journey.settings.saveFailed': 'Speichern fehlgeschlagen',
'journey.settings.coverUpdated': 'Titelbild aktualisiert',
'journey.settings.coverFailed': 'Upload fehlgeschlagen',
'journey.settings.failedToDelete': 'Löschen fehlgeschlagen',
'journey.entries.deleteTitle': 'Eintrag löschen',
'journey.photosUploaded': '{count} Fotos hochgeladen',
'journey.photosAdded': '{count} Fotos hinzugefügt',
'journey.public.notFound': 'Nicht gefunden',
'journey.public.notFoundMessage': 'Diese Journey existiert nicht oder der Link ist abgelaufen.',
'journey.public.readOnly': 'Nur lesen · Öffentliche Journey',
+16 -3
View File
@@ -5,7 +5,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'common.delete': 'Delete',
'common.edit': 'Edit',
'common.add': 'Add',
'common.loading': 'Loading...',
'common.loading': 'Loading',
'common.import': 'Import',
'common.error': 'Error',
'common.unknownError': 'Unknown error',
@@ -27,11 +27,15 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'common.name': 'Name',
'common.email': 'Email',
'common.password': 'Password',
'common.saving': 'Saving...',
'common.saving': 'Saving',
'common.justNow': 'just now',
'common.hoursAgo': '{count}h ago',
'common.daysAgo': '{count}d ago',
'common.saved': 'Saved',
'trips.memberRemoved': '{username} removed',
'trips.memberRemoveError': 'Failed to remove',
'trips.memberAdded': '{username} added',
'trips.memberAddError': 'Failed to add',
'trips.reminder': 'Reminder',
'trips.reminderNone': 'None',
'trips.reminderDay': 'day',
@@ -44,6 +48,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'common.uploading': 'Uploading…',
'common.backToPlanning': 'Back to Planning',
'common.reset': 'Reset',
'common.expand': 'Expand',
'common.collapse': 'Collapse',
// Navbar
'nav.trip': 'Trip',
@@ -961,6 +967,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'inspector.showHours': 'Show opening hours',
'inspector.files': 'Files',
'inspector.filesCount': '{count} files',
'inspector.remove': 'Remove',
'inspector.removeFromDay': 'Remove from Day',
'inspector.addToDay': 'Add to Day',
'inspector.confirmedRes': 'Confirmed Reservation',
@@ -1196,7 +1203,6 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'Check All',
'packing.menuUncheckAll': 'Uncheck All',
'packing.menuDeleteCat': 'Delete Category',
'packing.assignUser': 'Assign user',
'packing.noMembers': 'No trip members',
'packing.addItem': 'Add item',
'packing.addItemPlaceholder': 'Item name...',
@@ -1558,6 +1564,9 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'memories.error.addPhotos': 'Failed to add photos',
'memories.error.removePhoto': 'Failed to remove photo',
'memories.error.toggleSharing': 'Failed to update sharing',
'memories.saveRouteNotConfigured': 'Save route is not configured for this provider',
'memories.testRouteNotConfigured': 'Test route is not configured for this provider',
'memories.fillRequiredFields': 'Please fill all required fields',
// Collab Addon
'collab.tabs.chat': 'Chat',
@@ -1990,6 +1999,10 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'journey.settings.saveFailed': 'Failed to save',
'journey.settings.coverUpdated': 'Cover updated',
'journey.settings.coverFailed': 'Upload failed',
'journey.settings.failedToDelete': 'Failed to delete',
'journey.entries.deleteTitle': 'Delete Entry',
'journey.photosUploaded': '{count} photos uploaded',
'journey.photosAdded': '{count} photos added',
// Journey — Public Page
'journey.public.notFound': 'Not Found',
+14 -4
View File
@@ -29,11 +29,17 @@ const es: Record<string, string> = {
'common.password': 'Contraseña',
'common.saving': 'Guardando...',
'common.saved': 'Guardado',
'common.expand': 'Expandir',
'common.collapse': 'Contraer',
'trips.reminder': 'Recordatorio',
'trips.reminderNone': 'Ninguno',
'trips.reminderDay': 'día',
'trips.reminderDays': 'días',
'trips.reminderCustom': 'Personalizado',
'trips.memberRemoved': '{username} eliminado',
'trips.memberRemoveError': 'Error al eliminar',
'trips.memberAdded': '{username} añadido',
'trips.memberAddError': 'Error al añadir',
'trips.reminderDaysBefore': 'días antes de la salida',
'trips.reminderDisabledHint': 'Los recordatorios de viaje están desactivados. Actívalos en Admin > Configuración > Notificaciones.',
'common.update': 'Actualizar',
@@ -913,6 +919,7 @@ const es: Record<string, string> = {
'inspector.files': 'Archivos',
'inspector.filesCount': '{count} archivos',
'inspector.removeFromDay': 'Quitar del día',
'inspector.remove': 'Eliminar',
'inspector.addToDay': 'Añadir al día',
'inspector.confirmedRes': 'Reserva confirmada',
'inspector.pendingRes': 'Reserva pendiente',
@@ -1120,10 +1127,6 @@ const es: Record<string, string> = {
'packing.saveAsTemplate': 'Guardar como plantilla',
'packing.templateName': 'Nombre de la plantilla',
'packing.templateSaved': 'Lista de equipaje guardada como plantilla',
'packing.assignUser': 'Asignar usuario',
'packing.saveAsTemplate': 'Guardar como plantilla',
'packing.templateName': 'Nombre de la plantilla',
'packing.templateSaved': 'Lista de equipaje guardada como plantilla',
'packing.noMembers': 'Sin miembros',
'packing.bags': 'Equipaje',
'packing.noBag': 'Sin asignar',
@@ -1454,6 +1457,9 @@ const es: Record<string, string> = {
'memories.saved': 'Configuración de {provider_name} guardada',
'memories.providerDisconnectedBanner': 'Se perdió la conexión con {provider_name}. Vuelve a conectar en Configuración para ver las fotos.',
'memories.saveError': 'No se pudieron guardar los ajustes de {provider_name}',
'memories.saveRouteNotConfigured': 'La ruta de guardado no está configurada para este proveedor',
'memories.testRouteNotConfigured': 'La ruta de prueba no está configurada para este proveedor',
'memories.fillRequiredFields': 'Por favor complete todos los campos requeridos',
'memories.oldest': 'Más antiguas',
'memories.newest': 'Más recientes',
'memories.allLocations': 'Todas las ubicaciones',
@@ -1984,6 +1990,10 @@ const es: Record<string, string> = {
'journey.settings.saveFailed': 'No se pudo guardar',
'journey.settings.coverUpdated': 'Portada actualizada',
'journey.settings.coverFailed': 'Error al subir',
'journey.settings.failedToDelete': 'Error al eliminar',
'journey.entries.deleteTitle': 'Eliminar entrada',
'journey.photosUploaded': '{count} fotos subidas',
'journey.photosAdded': '{count} fotos añadidas',
'journey.public.notFound': 'No encontrado',
'journey.public.notFoundMessage': 'Esta travesía no existe o el enlace ha expirado.',
'journey.public.readOnly': 'Solo lectura · Travesía pública',
+14 -4
View File
@@ -29,6 +29,12 @@ const fr: Record<string, string> = {
'common.password': 'Mot de passe',
'common.saving': 'Enregistrement…',
'common.saved': 'Enregistré',
'trips.memberRemoved': '{username} supprimé',
'trips.memberRemoveError': 'Échec de la suppression',
'trips.memberAdded': '{username} ajouté',
'trips.memberAddError': "Échec de l'ajout",
'common.expand': 'Développer',
'common.collapse': 'Réduire',
'trips.reminder': 'Rappel',
'trips.reminderNone': 'Aucun',
'trips.reminderDay': 'jour',
@@ -936,6 +942,7 @@ const fr: Record<string, string> = {
'inspector.files': 'Fichiers',
'inspector.filesCount': '{count} fichiers',
'inspector.removeFromDay': 'Retirer du jour',
'inspector.remove': 'Supprimer',
'inspector.addToDay': 'Ajouter au jour',
'inspector.confirmedRes': 'Réservation confirmée',
'inspector.pendingRes': 'Réservation en attente',
@@ -1182,10 +1189,6 @@ const fr: Record<string, string> = {
'packing.saveAsTemplate': 'Enregistrer comme modèle',
'packing.templateName': 'Nom du modèle',
'packing.templateSaved': 'Liste de voyage enregistrée comme modèle',
'packing.assignUser': 'Assigner un utilisateur',
'packing.saveAsTemplate': 'Enregistrer comme modèle',
'packing.templateName': 'Nom du modèle',
'packing.templateSaved': 'Liste de bagages enregistrée comme modèle',
'packing.noMembers': 'Aucun membre',
'packing.bags': 'Bagages',
'packing.noBag': 'Non assigné',
@@ -1501,6 +1504,9 @@ const fr: Record<string, string> = {
'memories.saved': 'Paramètres {provider_name} enregistrés',
'memories.providerDisconnectedBanner': 'Votre connexion {provider_name} est perdue. Reconnectez-vous dans les Paramètres pour voir les photos.',
'memories.saveError': 'Impossible d\'enregistrer les paramètres de {provider_name}',
'memories.saveRouteNotConfigured': "La route de sauvegarde n'est pas configurée pour ce fournisseur",
'memories.testRouteNotConfigured': "La route de test n'est pas configurée pour ce fournisseur",
'memories.fillRequiredFields': 'Veuillez remplir tous les champs obligatoires',
'memories.oldest': 'Plus anciennes',
'memories.newest': 'Plus récentes',
'memories.allLocations': 'Tous les lieux',
@@ -1978,6 +1984,10 @@ const fr: Record<string, string> = {
'journey.settings.saveFailed': 'Échec de l\'enregistrement',
'journey.settings.coverUpdated': 'Couverture mise à jour',
'journey.settings.coverFailed': 'Échec du téléversement',
'journey.settings.failedToDelete': 'Échec de la suppression',
'journey.entries.deleteTitle': "Supprimer l'entrée",
'journey.photosUploaded': '{count} photos téléversées',
'journey.photosAdded': '{count} photos ajoutées',
'journey.public.notFound': 'Introuvable',
'journey.public.notFoundMessage': 'Ce journal n\'existe pas ou le lien a expiré.',
'journey.public.readOnly': 'Lecture seule · Journal public',
+14 -1
View File
@@ -28,6 +28,12 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'common.email': 'E-mail',
'common.password': 'Jelszó',
'common.saving': 'Mentés...',
'trips.memberRemoved': '{username} eltávolítva',
'trips.memberRemoveError': 'Eltávolítás sikertelen',
'trips.memberAdded': '{username} hozzáadva',
'trips.memberAddError': 'Hozzáadás sikertelen',
'common.expand': 'Kibontás',
'common.collapse': 'Összecsukás',
'common.saved': 'Mentve',
'trips.reminder': 'Emlékeztető',
'trips.reminderNone': 'Nincs',
@@ -937,6 +943,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'inspector.files': 'Fájlok',
'inspector.filesCount': '{count} fájl',
'inspector.removeFromDay': 'Eltávolítás a napról',
'inspector.remove': 'Eltávolítás',
'inspector.addToDay': 'Hozzáadás a naphoz',
'inspector.confirmedRes': 'Megerősített foglalás',
'inspector.pendingRes': 'Függőben lévő foglalás',
@@ -1172,7 +1179,6 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'Összes kipipálása',
'packing.menuUncheckAll': 'Összes jelölés törlése',
'packing.menuDeleteCat': 'Kategória törlése',
'packing.assignUser': 'Felhasználó hozzárendelése',
'packing.noMembers': 'Nincsenek utazási tagok',
'packing.addItem': 'Tétel hozzáadása',
'packing.addItemPlaceholder': 'Tétel neve...',
@@ -1807,6 +1813,9 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'memories.providerUsername': 'Felhasználónév',
'memories.providerPassword': 'Jelszó',
'memories.saveError': 'Nem sikerült menteni a(z) {provider_name} beállításait',
'memories.saveRouteNotConfigured': 'A mentési útvonal nincs konfigurálva ehhez a szolgáltatóhoz',
'memories.testRouteNotConfigured': 'A tesztútvonal nincs konfigurálva ehhez a szolgáltatóhoz',
'memories.fillRequiredFields': 'Kérjük töltse ki az összes kötelező mezőt',
'memories.selectAlbumMultiple': 'Album kiválasztása',
'memories.selectPhotosMultiple': 'Fotók kiválasztása',
'journey.title': 'Útinaplók',
@@ -1976,6 +1985,10 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'journey.settings.saveFailed': 'Nem sikerült menteni',
'journey.settings.coverUpdated': 'Borítókép frissítve',
'journey.settings.coverFailed': 'A feltöltés sikertelen',
'journey.settings.failedToDelete': 'Törlés sikertelen',
'journey.entries.deleteTitle': 'Bejegyzés törlése',
'journey.photosUploaded': '{count} fotó feltöltve',
'journey.photosAdded': '{count} fotó hozzáadva',
'journey.public.notFound': 'Nem található',
'journey.public.notFoundMessage': 'Ez az útinapló nem létezik vagy a link lejárt.',
'journey.public.readOnly': 'Csak olvasható · Nyilvános útinapló',
+14 -1
View File
@@ -29,6 +29,12 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'common.password': 'Password',
'common.saving': 'Salvataggio...',
'common.saved': 'Salvato',
'trips.memberRemoved': '{username} rimosso',
'trips.memberRemoveError': 'Rimozione non riuscita',
'trips.memberAdded': '{username} aggiunto',
'trips.memberAddError': 'Aggiunta non riuscita',
'common.expand': 'Espandi',
'common.collapse': 'Comprimi',
'trips.reminder': 'Promemoria',
'trips.reminderNone': 'Nessuno',
'trips.reminderDay': 'giorno',
@@ -937,6 +943,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'inspector.files': 'File',
'inspector.filesCount': '{count} file',
'inspector.removeFromDay': 'Rimuovi dal giorno',
'inspector.remove': 'Rimuovi',
'inspector.addToDay': 'Aggiungi al giorno',
'inspector.confirmedRes': 'Prenotazione confermata',
'inspector.pendingRes': 'Prenotazione in attesa',
@@ -1172,7 +1179,6 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'Seleziona tutti',
'packing.menuUncheckAll': 'Deseleziona tutti',
'packing.menuDeleteCat': 'Elimina categoria',
'packing.assignUser': 'Assegna utente',
'packing.noMembers': 'Nessun membro del viaggio',
'packing.addItem': 'Aggiungi elemento',
'packing.addItemPlaceholder': 'Nome elemento...',
@@ -1499,6 +1505,9 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'memories.saved': 'Impostazioni {provider_name} salvate',
'memories.providerDisconnectedBanner': 'La connessione a {provider_name} è persa. Riconnetti nelle Impostazioni per visualizzare le foto.',
'memories.saveError': 'Impossibile salvare le impostazioni di {provider_name}',
'memories.saveRouteNotConfigured': 'La route di salvataggio non è configurata per questo provider',
'memories.testRouteNotConfigured': 'La route di test non è configurata per questo provider',
'memories.fillRequiredFields': 'Per favore compila tutti i campi obbligatori',
'memories.addPhotos': 'Aggiungi foto',
'memories.linkAlbum': 'Collega album',
'memories.selectAlbum': 'Seleziona album Immich',
@@ -1976,6 +1985,10 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'journey.settings.saveFailed': 'Salvataggio fallito',
'journey.settings.coverUpdated': 'Copertina aggiornata',
'journey.settings.coverFailed': 'Caricamento fallito',
'journey.settings.failedToDelete': 'Eliminazione non riuscita',
'journey.entries.deleteTitle': 'Elimina voce',
'journey.photosUploaded': '{count} foto caricate',
'journey.photosAdded': '{count} foto aggiunte',
'journey.public.notFound': 'Non trovato',
'journey.public.notFoundMessage': 'Questo diario non esiste o il link è scaduto.',
'journey.public.readOnly': 'Sola lettura · Diario pubblico',
+14 -1
View File
@@ -29,6 +29,12 @@ const nl: Record<string, string> = {
'common.password': 'Wachtwoord',
'common.saving': 'Opslaan...',
'common.saved': 'Opgeslagen',
'trips.memberRemoved': '{username} verwijderd',
'trips.memberRemoveError': 'Verwijderen mislukt',
'trips.memberAdded': '{username} toegevoegd',
'trips.memberAddError': 'Toevoegen mislukt',
'common.expand': 'Uitvouwen',
'common.collapse': 'Inklappen',
'trips.reminder': 'Herinnering',
'trips.reminderNone': 'Geen',
'trips.reminderDay': 'dag',
@@ -936,6 +942,7 @@ const nl: Record<string, string> = {
'inspector.files': 'Bestanden',
'inspector.filesCount': '{count} bestanden',
'inspector.removeFromDay': 'Verwijderen van dag',
'inspector.remove': 'Verwijderen',
'inspector.addToDay': 'Toevoegen aan dag',
'inspector.confirmedRes': 'Bevestigde reservering',
'inspector.pendingRes': 'Reservering in behandeling',
@@ -1171,7 +1178,6 @@ const nl: Record<string, string> = {
'packing.menuCheckAll': 'Alles aanvinken',
'packing.menuUncheckAll': 'Alles uitvinken',
'packing.menuDeleteCat': 'Categorie verwijderen',
'packing.assignUser': 'Gebruiker toewijzen',
'packing.addItem': 'Item toevoegen',
'packing.addItemPlaceholder': 'Itemnaam...',
'packing.addCategory': 'Categorie toevoegen',
@@ -1498,6 +1504,9 @@ const nl: Record<string, string> = {
'memories.saved': '{provider_name}-instellingen opgeslagen',
'memories.providerDisconnectedBanner': 'Je {provider_name}-verbinding is verbroken. Maak opnieuw verbinding in Instellingen om foto\'s te bekijken.',
'memories.saveError': '{provider_name}-instellingen konden niet worden opgeslagen',
'memories.saveRouteNotConfigured': 'Opslagroute is niet geconfigureerd voor deze provider',
'memories.testRouteNotConfigured': 'Testroute is niet geconfigureerd voor deze provider',
'memories.fillRequiredFields': 'Vul alle verplichte velden in',
'memories.oldest': 'Oudste eerst',
'memories.newest': 'Nieuwste eerst',
'memories.allLocations': 'Alle locaties',
@@ -1975,6 +1984,10 @@ const nl: Record<string, string> = {
'journey.settings.saveFailed': 'Opslaan mislukt',
'journey.settings.coverUpdated': 'Omslag bijgewerkt',
'journey.settings.coverFailed': 'Uploaden mislukt',
'journey.settings.failedToDelete': 'Verwijderen mislukt',
'journey.entries.deleteTitle': 'Vermelding verwijderen',
'journey.photosUploaded': "{count} foto's geüpload",
'journey.photosAdded': "{count} foto's toegevoegd",
'journey.public.notFound': 'Niet gevonden',
'journey.public.notFoundMessage': 'Dit reisverslag bestaat niet of de link is verlopen.',
'journey.public.readOnly': 'Alleen-lezen · Openbaar reisverslag',
+14 -1
View File
@@ -27,6 +27,12 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'common.email': 'E-mail',
'common.password': 'Hasło',
'common.saving': 'Zapisywanie...',
'trips.memberRemoved': '{username} usunięty',
'trips.memberRemoveError': 'Nie udało się usunąć',
'trips.memberAdded': '{username} dodany',
'trips.memberAddError': 'Nie udało się dodać',
'common.expand': 'Rozwiń',
'common.collapse': 'Zwiń',
'common.update': 'Aktualizuj',
'common.change': 'Zmień',
'common.uploading': 'Przesyłanie...',
@@ -898,6 +904,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'inspector.files': 'Pliki',
'inspector.filesCount': '{count} plików',
'inspector.removeFromDay': 'Usuń z dnia',
'inspector.remove': 'Usuń',
'inspector.addToDay': 'Dodaj do dnia',
'inspector.confirmedRes': 'Potwierdzona rezerwacja',
'inspector.pendingRes': 'Oczekująca rezerwacja',
@@ -1129,7 +1136,6 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'Zaznacz wszystko',
'packing.menuUncheckAll': 'Odznacz wszystko',
'packing.menuDeleteCat': 'Usuń kategorię',
'packing.assignUser': 'Przypisz użytkownika',
'packing.saveAsTemplate': 'Zapisz jako szablon',
'packing.templateName': 'Nazwa szablonu',
'packing.templateSaved': 'Lista pakowania zapisana jako szablon',
@@ -1802,6 +1808,9 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'memories.providerUsername': 'Nazwa użytkownika',
'memories.providerPassword': 'Hasło',
'memories.saveError': 'Nie udało się zapisać ustawień {provider_name}',
'memories.saveRouteNotConfigured': 'Trasa zapisu nie jest skonfigurowana dla tego dostawcy',
'memories.testRouteNotConfigured': 'Trasa testowa nie jest skonfigurowana dla tego dostawcy',
'memories.fillRequiredFields': 'Proszę wypełnić wszystkie wymagane pola',
'memories.selectAlbumMultiple': 'Wybierz album',
'memories.selectPhotosMultiple': 'Wybierz zdjęcia',
'journey.title': 'Dziennik podróży',
@@ -1971,6 +1980,10 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'journey.settings.saveFailed': 'Zapisywanie nie powiodło się',
'journey.settings.coverUpdated': 'Okładka zaktualizowana',
'journey.settings.coverFailed': 'Przesyłanie nie powiodło się',
'journey.settings.failedToDelete': 'Nie udało się usunąć',
'journey.entries.deleteTitle': 'Usuń wpis',
'journey.photosUploaded': '{count} zdjęć przesłanych',
'journey.photosAdded': '{count} zdjęć dodanych',
'journey.public.notFound': 'Nie znaleziono',
'journey.public.notFoundMessage': 'Ten dziennik podróży nie istnieje lub link wygasł.',
'journey.public.readOnly': 'Tylko do odczytu · Publiczny dziennik podróży',
+14 -1
View File
@@ -29,6 +29,12 @@ const ru: Record<string, string> = {
'common.password': 'Пароль',
'common.saving': 'Сохранение...',
'common.saved': 'Сохранено',
'common.expand': 'Развернуть',
'common.collapse': 'Свернуть',
'trips.memberRemoved': '{username} удалён',
'trips.memberRemoveError': 'Не удалось удалить',
'trips.memberAdded': '{username} добавлен',
'trips.memberAddError': 'Не удалось добавить',
'trips.reminder': 'Напоминание',
'trips.reminderNone': 'Нет',
'trips.reminderDay': 'день',
@@ -936,6 +942,7 @@ const ru: Record<string, string> = {
'inspector.files': 'Файлы',
'inspector.filesCount': '{count} файлов',
'inspector.removeFromDay': 'Убрать из дня',
'inspector.remove': 'Удалить',
'inspector.addToDay': 'Добавить в день',
'inspector.confirmedRes': 'Подтверждённое бронирование',
'inspector.pendingRes': 'Ожидающее бронирование',
@@ -1182,7 +1189,6 @@ const ru: Record<string, string> = {
'packing.saveAsTemplate': 'Сохранить как шаблон',
'packing.templateName': 'Название шаблона',
'packing.templateSaved': 'Список вещей сохранён как шаблон',
'packing.assignUser': 'Назначить пользователя',
'packing.noMembers': 'Нет участников',
'packing.bags': 'Багаж',
'packing.noBag': 'Не назначено',
@@ -1806,6 +1812,9 @@ const ru: Record<string, string> = {
'memories.providerUsername': 'Имя пользователя',
'memories.providerPassword': 'Пароль',
'memories.saveError': 'Не удалось сохранить настройки {provider_name}',
'memories.saveRouteNotConfigured': 'Маршрут сохранения не настроен для этого провайдера',
'memories.testRouteNotConfigured': 'Маршрут тестирования не настроен для этого провайдера',
'memories.fillRequiredFields': 'Пожалуйста, заполните все обязательные поля',
'memories.selectAlbumMultiple': 'Выбрать альбом',
'memories.selectPhotosMultiple': 'Выбрать фото',
'journey.title': 'Путешествие',
@@ -1975,6 +1984,10 @@ const ru: Record<string, string> = {
'journey.settings.saveFailed': 'Не удалось сохранить',
'journey.settings.coverUpdated': 'Обложка обновлена',
'journey.settings.coverFailed': 'Загрузка не удалась',
'journey.settings.failedToDelete': 'Не удалось удалить',
'journey.entries.deleteTitle': 'Удалить запись',
'journey.photosUploaded': '{count} фото загружено',
'journey.photosAdded': '{count} фото добавлено',
'journey.public.notFound': 'Не найдено',
'journey.public.notFoundMessage': 'Это путешествие не существует или ссылка устарела.',
'journey.public.readOnly': 'Только для чтения · Публичное путешествие',
+14 -1
View File
@@ -29,6 +29,12 @@ const zh: Record<string, string> = {
'common.password': '密码',
'common.saving': '保存中...',
'common.saved': '已保存',
'common.expand': '展开',
'common.collapse': '折叠',
'trips.memberRemoved': '{username} 已移除',
'trips.memberRemoveError': '移除失败',
'trips.memberAdded': '{username} 已添加',
'trips.memberAddError': '添加失败',
'trips.reminder': '提醒',
'trips.reminderNone': '无',
'trips.reminderDay': '天',
@@ -936,6 +942,7 @@ const zh: Record<string, string> = {
'inspector.files': '文件',
'inspector.filesCount': '{count} 个文件',
'inspector.removeFromDay': '从当天移除',
'inspector.remove': '删除',
'inspector.addToDay': '添加到当天',
'inspector.confirmedRes': '已确认预订',
'inspector.pendingRes': '待确认预订',
@@ -1182,7 +1189,6 @@ const zh: Record<string, string> = {
'packing.saveAsTemplate': '保存为模板',
'packing.templateName': '模板名称',
'packing.templateSaved': '行李清单已保存为模板',
'packing.assignUser': '分配用户',
'packing.noMembers': '无成员',
'packing.bags': '行李',
'packing.noBag': '未分配',
@@ -1806,6 +1812,9 @@ const zh: Record<string, string> = {
'memories.providerUsername': '用户名',
'memories.providerPassword': '密码',
'memories.saveError': '无法保存 {provider_name} 设置',
'memories.saveRouteNotConfigured': '此提供商未配置保存路由',
'memories.testRouteNotConfigured': '此提供商未配置测试路由',
'memories.fillRequiredFields': '请填写所有必填字段',
'memories.selectAlbumMultiple': '选择相册',
'memories.selectPhotosMultiple': '选择照片',
'journey.title': '旅程',
@@ -1975,6 +1984,10 @@ const zh: Record<string, string> = {
'journey.settings.saveFailed': '保存失败',
'journey.settings.coverUpdated': '封面已更新',
'journey.settings.coverFailed': '上传失败',
'journey.settings.failedToDelete': '删除失败',
'journey.entries.deleteTitle': '删除条目',
'journey.photosUploaded': '{count} 张照片已上传',
'journey.photosAdded': '{count} 张照片已添加',
'journey.public.notFound': '未找到',
'journey.public.notFoundMessage': '此旅程不存在或链接已过期。',
'journey.public.readOnly': '只读 · 公开旅程',
+14 -1
View File
@@ -29,6 +29,12 @@ const zhTw: Record<string, string> = {
'common.password': '密碼',
'common.saving': '儲存中...',
'common.saved': '已儲存',
'common.expand': '展開',
'common.collapse': '折疊',
'trips.memberRemoved': '{username} 已移除',
'trips.memberRemoveError': '移除失敗',
'trips.memberAdded': '{username} 已新增',
'trips.memberAddError': '新增失敗',
'trips.reminder': '提醒',
'trips.reminderNone': '無',
'trips.reminderDay': '天',
@@ -963,6 +969,7 @@ const zhTw: Record<string, string> = {
'inspector.files': '檔案',
'inspector.filesCount': '{count} 個檔案',
'inspector.removeFromDay': '從當天移除',
'inspector.remove': '刪除',
'inspector.addToDay': '新增到當天',
'inspector.confirmedRes': '已確認預訂',
'inspector.pendingRes': '待確認預訂',
@@ -1197,7 +1204,6 @@ const zhTw: Record<string, string> = {
'packing.menuCheckAll': '全部勾選',
'packing.menuUncheckAll': '取消全部勾選',
'packing.menuDeleteCat': '刪除分類',
'packing.assignUser': '指派使用者',
'packing.addItem': '新增物品',
'packing.addItemPlaceholder': '物品名稱...',
'packing.addCategory': '新增分類',
@@ -1767,6 +1773,9 @@ const zhTw: Record<string, string> = {
'memories.providerUsername': '使用者名稱',
'memories.providerPassword': '密碼',
'memories.saveError': '無法儲存 {provider_name} 設定',
'memories.saveRouteNotConfigured': '此提供商未設定儲存路由',
'memories.testRouteNotConfigured': '此提供商未設定測試路由',
'memories.fillRequiredFields': '請填寫所有必填欄位',
'memories.selectAlbumMultiple': '選擇相簿',
'memories.selectPhotosMultiple': '選擇照片',
'journey.title': '旅程',
@@ -1936,6 +1945,10 @@ const zhTw: Record<string, string> = {
'journey.settings.saveFailed': '儲存失敗',
'journey.settings.coverUpdated': '封面已更新',
'journey.settings.coverFailed': '上傳失敗',
'journey.settings.failedToDelete': '刪除失敗',
'journey.entries.deleteTitle': '刪除條目',
'journey.photosUploaded': '{count} 張照片已上傳',
'journey.photosAdded': '{count} 張照片已新增',
'journey.public.notFound': '未找到',
'journey.public.notFoundMessage': '此旅程不存在或連結已過期。',
'journey.public.readOnly': '唯讀 · 公開旅程',
+31 -31
View File
@@ -559,9 +559,9 @@ export default function JourneyDetailPage() {
setDeleteTarget(null)
loadJourney(Number(id))
}}
title="Delete Entry"
message={`Delete "${deleteTarget?.title || 'this entry'}"? This cannot be undone.`}
confirmLabel="Delete"
title={t('journey.entries.deleteTitle')}
message={t('journey.deleteConfirmMessage', { title: deleteTarget?.title || 'this entry' })}
confirmLabel={t('common.delete')}
danger
/>
@@ -580,9 +580,9 @@ export default function JourneyDetailPage() {
toast.error(t('journey.trips.unlinkFailed'))
}
}}
title="Unlink Trip"
message={`Unlink "${unlinkTrip?.title}"? All synced entries and photos from this trip will be permanently deleted. This cannot be undone.`}
confirmLabel="Unlink"
title={t('journey.trips.unlinkTrip')}
message={t('journey.trips.unlinkMessage', { title: unlinkTrip?.title })}
confirmLabel={t('journey.trips.unlink')}
danger
/>
@@ -807,7 +807,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
for (const f of files) formData.append('photos', f)
try {
await journeyApi.uploadPhotos(entryId, formData)
toast.success(`${files.length} photos uploaded`)
toast.success(t('journey.photosUploaded', { count: files.length }))
onRefresh()
} catch {
toast.error(t('journey.settings.coverFailed'))
@@ -934,7 +934,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
} catch {}
}
if (added > 0) {
toast.success(`${added} photos added`)
toast.success(t('journey.photosAdded', { count: added }))
onRefresh()
}
setShowPicker(false)
@@ -1175,8 +1175,8 @@ function EntryCard({ entry, onEdit, onDelete, onPhotoClick }: {
<>
<div className="fixed inset-0 z-[99]" onClick={() => setMenuOpen(false)} />
<div className="fixed z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg py-1 min-w-[120px]" style={{ top: (menuBtnRef.current?.getBoundingClientRect().bottom || 0) + 4, right: window.innerWidth - (menuBtnRef.current?.getBoundingClientRect().right || 0) }}>
<button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> Edit</button>
<button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> Delete</button>
<button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> {t('common.edit')}</button>
<button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> {t('common.delete')}</button>
</div>
</>
)}
@@ -2173,7 +2173,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
<button onClick={handleSave} disabled={saving} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-50">
{saving ? 'Saving...' : 'Save'}
{saving ? t('common.saving') : t('common.save')}
</button>
</div>
</div>
@@ -2542,7 +2542,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
await updateJourney(journey.id, { title, subtitle: subtitle || null })
onSaved()
} catch {
toast.error('Failed to save')
toast.error(t('journey.settings.saveFailed'))
} finally {
setSaving(false)
}
@@ -2555,10 +2555,10 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
formData.append('cover', file)
try {
await journeyApi.uploadCover(journey.id, formData)
toast.success('Cover updated')
toast.success(t('journey.settings.coverUpdated'))
onSaved()
} catch {
toast.error('Upload failed')
toast.error(t('journey.settings.coverFailed'))
}
}
@@ -2569,7 +2569,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
await deleteJourney(journey.id)
navigate('/journey')
} catch {
toast.error('Failed to delete')
toast.error(t('journey.settings.failedToDelete'))
}
}
@@ -2629,14 +2629,14 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
{/* Synced Trips */}
<div>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">Synced Trips</label>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.syncedTrips')}</label>
<div className="flex flex-col gap-1.5">
{journey.trips.map((trip: any) => (
<div key={trip.trip_id} className="flex items-center gap-2.5 p-2 rounded-lg bg-zinc-50 dark:bg-zinc-800">
<div className="w-8 h-8 rounded-md flex-shrink-0" style={{ background: pickGradient(trip.trip_id) }} />
<div className="flex-1 min-w-0">
<div className="text-[12px] font-medium text-zinc-900 dark:text-white">{trip.title}</div>
<div className="text-[10px] text-zinc-500">{trip.place_count || 0} places</div>
<div className="text-[10px] text-zinc-500">{trip.place_count || 0} {t('journey.synced.places')}</div>
</div>
<button
onClick={() => setUnlinkTarget({ trip_id: trip.trip_id, title: trip.title })}
@@ -2647,19 +2647,19 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
</button>
</div>
))}
{journey.trips.length === 0 && <p className="text-[11px] text-zinc-400">No trips linked</p>}
{journey.trips.length === 0 && <p className="text-[11px] text-zinc-400">{t('journey.trips.noTripsLinkedSettings')}</p>}
<button
onClick={() => setShowAddTrip(true)}
className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
>
<Plus size={14} /> Add Trip
<Plus size={14} /> {t('journey.trips.addTrip')}
</button>
</div>
</div>
{/* Contributors */}
<div>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">Contributors</label>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.contributors')}</label>
<div className="flex flex-col gap-2">
{journey.contributors.map((c: any) => (
<div key={c.user_id} className="flex items-center gap-2.5">
@@ -2674,7 +2674,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
onClick={onOpenInvite}
className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
>
<UserPlus size={14} /> Invite Contributor
<UserPlus size={14} /> {t('journey.contributors.invite')}
</button>
</div>
</div>
@@ -2693,11 +2693,11 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
className="flex items-center gap-1.5 text-[12px] font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg px-2.5 py-2 mr-auto"
>
<Trash2 size={13} />
Delete
{t('journey.settings.delete')}
</button>
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
<button onClick={handleSave} disabled={saving || !title.trim()} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40">
{saving ? 'Saving...' : 'Save'}
{saving ? t('common.saving') : t('common.save')}
</button>
</div>
</div>
@@ -2710,16 +2710,16 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
if (!unlinkTarget) return
try {
await journeyApi.removeTrip(journey.id, unlinkTarget.trip_id)
toast.success('Trip unlinked')
toast.success(t('journey.trips.tripUnlinked'))
setUnlinkTarget(null)
onSaved()
} catch {
toast.error('Failed to unlink trip')
toast.error(t('journey.trips.unlinkFailed'))
}
}}
title="Unlink Trip"
message={`Unlink "${unlinkTarget?.title}"? All synced entries and photos from this trip will be permanently deleted. This cannot be undone.`}
confirmLabel="Unlink"
title={t('journey.trips.unlinkTrip')}
message={t('journey.trips.unlinkMessage', { title: unlinkTarget?.title })}
confirmLabel={t('journey.trips.unlink')}
danger
/>
@@ -2737,9 +2737,9 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
isOpen={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
onConfirm={handleDelete}
title="Delete Journey"
message={`Delete "${journey.title}"? All entries and photos will be lost.`}
confirmLabel="Delete"
title={t('journey.settings.deleteJourney')}
message={t('journey.settings.deleteMessage', { title: journey.title })}
confirmLabel={t('common.delete')}
danger
/>
</div>