fix(costs): rework the cost panel UX wise and apply prettier on the shared package

This commit is contained in:
jubnl
2026-06-18 13:59:10 +02:00
parent ad6e1ddcc8
commit d5850041a7
584 changed files with 6915 additions and 10724 deletions
+1
View File
@@ -595,6 +595,7 @@ export const budgetApi = {
perPersonSummary: (tripId: number | string) => apiClient.get(`/trips/${tripId}/budget/summary/per-person`).then(r => r.data), perPersonSummary: (tripId: number | string) => apiClient.get(`/trips/${tripId}/budget/summary/per-person`).then(r => r.data),
settlement: (tripId: number | string, base?: string) => apiClient.get(`/trips/${tripId}/budget/settlement`, base ? { params: { base } } : undefined).then(r => r.data), settlement: (tripId: number | string, base?: string) => apiClient.get(`/trips/${tripId}/budget/settlement`, base ? { params: { base } } : undefined).then(r => r.data),
createSettlement: (tripId: number | string, data: { from_user_id: number; to_user_id: number; amount: number }) => apiClient.post(`/trips/${tripId}/budget/settlements`, data).then(r => r.data), createSettlement: (tripId: number | string, data: { from_user_id: number; to_user_id: number; amount: number }) => apiClient.post(`/trips/${tripId}/budget/settlements`, data).then(r => r.data),
updateSettlement: (tripId: number | string, settlementId: number, data: { from_user_id: number; to_user_id: number; amount: number }) => apiClient.put(`/trips/${tripId}/budget/settlements/${settlementId}`, data).then(r => r.data),
deleteSettlement: (tripId: number | string, settlementId: number) => apiClient.delete(`/trips/${tripId}/budget/settlements/${settlementId}`).then(r => r.data), deleteSettlement: (tripId: number | string, settlementId: number) => apiClient.delete(`/trips/${tripId}/budget/settlements/${settlementId}`).then(r => r.data),
reorderItems: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/budget/reorder/items`, { orderedIds }).then(r => r.data), reorderItems: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/budget/reorder/items`, { orderedIds }).then(r => r.data),
reorderCategories: (tripId: number | string, orderedCategories: string[]) => apiClient.put(`/trips/${tripId}/budget/reorder/categories`, { orderedCategories } satisfies BudgetReorderCategoriesRequest).then(r => r.data), reorderCategories: (tripId: number | string, orderedCategories: string[]) => apiClient.put(`/trips/${tripId}/budget/reorder/categories`, { orderedCategories } satisfies BudgetReorderCategoriesRequest).then(r => r.data),
@@ -0,0 +1,138 @@
// FE-COMP-COSTS: settlements surfaced inline in the Costs ledger (issue #1241)
import { render, screen, waitFor } from '../../../tests/helpers/render'
import { http, HttpResponse } from 'msw'
import { server } from '../../../tests/helpers/msw/server'
import { useAuthStore } from '../../store/authStore'
import { useTripStore } from '../../store/tripStore'
import { resetAllStores, seedStore } from '../../../tests/helpers/store'
import { buildUser, buildTrip, buildBudgetItem } from '../../../tests/helpers/factories'
import CostsPanel from './CostsPanel'
const tripMembers = [
{ id: 1, username: 'alice', avatar_url: null },
{ id: 2, username: 'bob', avatar_url: null },
]
beforeEach(() => {
resetAllStores()
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true })
seedStore(useTripStore, { trip: buildTrip({ id: 1, currency: 'EUR' }) })
})
describe('CostsPanel — settlements in the ledger', () => {
it('renders a settle-up payment as a ledger row with an undo action', async () => {
const item = { ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Dinner' }), total_price: 90, expense_date: '2025-06-15' }
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })),
http.get('/api/trips/1/budget/settlement', () =>
HttpResponse.json({
balances: [],
flows: [],
settlements: [
{ id: 7, trip_id: 1, from_user_id: 2, to_user_id: 1, amount: 30, created_at: '2025-06-16 10:00:00', from_username: 'bob', to_username: 'alice' },
],
})
),
)
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
// The expense and the settlement (payment) both appear in the unified ledger.
await screen.findByText('Dinner')
await screen.findByText('Payment')
// The payment row exposes an inline undo (no need to open a separate History modal).
expect(screen.getByTitle('Undo')).toBeInTheDocument()
})
it('records a manual payment via the Add payment button', async () => {
let posted: Record<string, unknown> | null = null
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })),
http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })),
http.post('/api/trips/1/budget/settlements', async ({ request }) => {
posted = await request.json() as Record<string, unknown>
return HttpResponse.json({ settlement: { id: 1, ...posted } })
}),
)
const { default: userEvent } = await import('@testing-library/user-event')
const user = userEvent.setup()
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
await user.click(await screen.findByRole('button', { name: 'Add payment' }))
await user.type(await screen.findByPlaceholderText('0.00'), '25')
// The footer submit is the second "Add payment" control once the modal is open.
const addButtons = screen.getAllByRole('button', { name: 'Add payment' })
const submit = addButtons[addButtons.length - 1]
await user.click(submit)
await waitFor(() => expect(posted).toMatchObject({ amount: 25 }))
})
it('hides payment rows while a text search is active', async () => {
const item = { ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Dinner' }), total_price: 90, expense_date: '2025-06-15' }
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })),
http.get('/api/trips/1/budget/settlement', () =>
HttpResponse.json({
balances: [],
flows: [],
settlements: [
{ id: 7, trip_id: 1, from_user_id: 2, to_user_id: 1, amount: 30, created_at: '2025-06-16 10:00:00', from_username: 'bob', to_username: 'alice' },
],
})
),
)
const { default: userEvent } = await import('@testing-library/user-event')
const user = userEvent.setup()
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
await screen.findByText('Payment')
await user.type(screen.getByPlaceholderText('Search expenses…'), 'Dinner')
// Payment rows have no name, so a search hides them while the matching expense stays.
expect(screen.queryByText('Payment')).not.toBeInTheDocument()
expect(screen.getByText('Dinner')).toBeInTheDocument()
})
it('auto-splits the total across participants and rebalances a pinned amount on save', async () => {
let posted: Record<string, unknown> | null = null
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })),
http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })),
http.post('/api/trips/1/budget', async ({ request }) => {
posted = await request.json() as Record<string, unknown>
return HttpResponse.json({ item: { ...buildBudgetItem({ trip_id: 1, name: 'Dinner' }), id: 5 } })
}),
)
const { default: userEvent } = await import('@testing-library/user-event')
const user = userEvent.setup()
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
await user.click(await screen.findByRole('button', { name: 'Add expense' }))
await user.type(await screen.findByPlaceholderText('e.g. Dinner, souvenirs, gas…'), 'Dinner')
const nums = () => screen.getAllByRole('spinbutton') as HTMLInputElement[]
await user.type(nums()[0], '100') // total → auto equal-split across the 2 participants
await waitFor(() => expect(nums()[1].value).toBe('50'))
expect(nums()[2].value).toBe('50')
// Pin the first participant to 30 → the other non-pinned field rebalances to 70.
await user.clear(nums()[1]); await user.type(nums()[1], '30')
await waitFor(() => expect(nums()[2].value).toBe('70'))
const addBtns = screen.getAllByRole('button', { name: 'Add expense' })
await user.click(addBtns[addBtns.length - 1]) // footer submit
await waitFor(() => expect(posted).toBeTruthy())
expect(posted!.total_price).toBe(100)
expect(posted!.payers).toEqual(expect.arrayContaining([
expect.objectContaining({ user_id: 1, amount: 30 }),
expect.objectContaining({ user_id: 2, amount: 70 }),
]))
})
it('marks an expense with no payer as Unfinished', async () => {
const item = { ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Hotel' }), total_price: 90, payers: [], members: [{ user_id: 1, username: 'alice', paid: 0 }] }
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })),
http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })),
)
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
await screen.findByText('Hotel')
expect(screen.getByText('Unfinished')).toBeInTheDocument()
})
})
+237 -97
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useMemo, useCallback } from 'react' import { useState, useEffect, useMemo, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { ArrowDown, ArrowUp, BarChart3, Plus, Search, ArrowRight, Check, RotateCcw, History, Pencil, Trash2 } from 'lucide-react' import { ArrowDown, ArrowUp, BarChart3, Plus, Search, ArrowRight, ArrowLeftRight, Check, RotateCcw, Pencil, Trash2 } from 'lucide-react'
import { useTripStore } from '../../store/tripStore' import { useTripStore } from '../../store/tripStore'
import { useAuthStore } from '../../store/authStore' import { useAuthStore } from '../../store/authStore'
import { useSettingsStore } from '../../store/settingsStore' import { useSettingsStore } from '../../store/settingsStore'
@@ -39,6 +39,12 @@ interface SettlementData {
settlements: Settlement[] settlements: Settlement[]
} }
// One row in the unified Costs ledger — either an expense or a settle-up payment,
// carrying the date used to group it by day.
type LedgerEntry =
| { kind: 'expense'; date: string; e: BudgetItem }
| { kind: 'payment'; date: string; s: Settlement }
const round2 = (n: number) => Math.round(n * 100) / 100 const round2 = (n: number) => Math.round(n * 100) / 100
const FIELD_H = 40 // shared height for the amount / currency / day row in the modal const FIELD_H = 40 // shared height for the amount / currency / day row in the modal
@@ -62,9 +68,10 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
const [settlement, setSettlement] = useState<SettlementData | null>(null) const [settlement, setSettlement] = useState<SettlementData | null>(null)
const [filter, setFilter] = useState<'all' | 'mine' | 'owed'>('all') const [filter, setFilter] = useState<'all' | 'mine' | 'owed'>('all')
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [histOpen, setHistOpen] = useState(false)
const [modalOpen, setModalOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false)
const [editing, setEditing] = useState<BudgetItem | null>(null) const [editing, setEditing] = useState<BudgetItem | null>(null)
const [editingSettlement, setEditingSettlement] = useState<Settlement | null>(null)
const [addingPayment, setAddingPayment] = useState(false)
const people = tripMembers const people = tripMembers
const personById = useCallback((id: number) => people.find(p => p.id === id), [people]) const personById = useCallback((id: number) => people.find(p => p.id === id), [people])
@@ -122,21 +129,37 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
return list return list
}, [budgetItems, filter, search, me]) }, [budgetItems, filter, search, me])
// Settlements ("payments") shown inline in the ledger. They have no name, so a
// text search hides them; they're excluded from the "owed" expense filter and,
// under "mine", only show transfers I'm part of.
const filteredSettlements = useMemo(() => {
if (search.trim()) return []
if (filter === 'owed') return []
let list = settlement?.settlements || []
if (filter === 'mine') list = list.filter(s => s.from_user_id === me || s.to_user_id === me)
return list
}, [settlement, filter, search, me])
const dayGroups = useMemo(() => { const dayGroups = useMemo(() => {
const groups: { day: string; items: BudgetItem[] }[] = [] const entries: LedgerEntry[] = [
const labelOf = (e: BudgetItem) => { ...filtered.map(e => ({ kind: 'expense' as const, date: e.expense_date || '', e })),
if (!e.expense_date) return t('costs.noDate') ...filteredSettlements.map(s => ({ kind: 'payment' as const, date: (s.created_at || '').slice(0, 10), s })),
try { return new Date(e.expense_date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) } catch { return e.expense_date } ]
const labelOf = (date: string) => {
if (!date) return t('costs.noDate')
try { return new Date(date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) } catch { return date }
} }
const sorted = filtered.slice().sort((a, b) => (b.expense_date || '').localeCompare(a.expense_date || '')) // Newest day first; within a day, expenses before payments (insertion order).
for (const e of sorted) { const sorted = entries.slice().sort((a, b) => (b.date || '').localeCompare(a.date || ''))
const day = labelOf(e) const groups: { day: string; entries: LedgerEntry[] }[] = []
for (const en of sorted) {
const day = labelOf(en.date)
let g = groups.find(x => x.day === day) let g = groups.find(x => x.day === day)
if (!g) { g = { day, items: [] }; groups.push(g) } if (!g) { g = { day, entries: [] }; groups.push(g) }
g.items.push(e) g.entries.push(en)
} }
return groups return groups
}, [filtered, locale, t]) }, [filtered, filteredSettlements, locale, t])
// ── settle actions ────────────────────────────────────────────────────── // ── settle actions ──────────────────────────────────────────────────────
const settleFlow = async (fromId: number, toId: number, amount: number) => { const settleFlow = async (fromId: number, toId: number, amount: number) => {
@@ -280,14 +303,16 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
{search ? t('costs.noMatch') : t('costs.emptyText')} {search ? t('costs.noMatch') : t('costs.emptyText')}
</div> </div>
) : dayGroups.map(g => { ) : dayGroups.map(g => {
const dtot = g.items.reduce((a, e) => a + baseTotal(e), 0) const dtot = g.entries.reduce((a, en) => en.kind === 'expense' ? a + baseTotal(en.e) : a, 0)
return ( return (
<div key={g.day} style={{ marginBottom: 22 }}> <div key={g.day} style={{ marginBottom: 22 }}>
<div className={labelCls} style={{ display: 'flex', alignItems: 'center', margin: '0 0 10px 4px' }}> <div className={labelCls} style={{ display: 'flex', alignItems: 'center', margin: '0 0 10px 4px' }}>
{g.day}<span className="text-content-muted" style={{ marginLeft: 'auto', textTransform: 'none', letterSpacing: 0, fontWeight: 500, fontSize: 12 }}>{t('costs.spent', { amount: fmt(dtot) })}</span> {g.day}<span className="text-content-muted" style={{ marginLeft: 'auto', textTransform: 'none', letterSpacing: 0, fontWeight: 500, fontSize: 12 }}>{t('costs.spent', { amount: fmt(dtot) })}</span>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{g.items.map(e => <ExpenseRow key={e.id} e={e} />)} {g.entries.map(en => en.kind === 'expense'
? <ExpenseRow key={'e' + en.e.id} e={en.e} />
: <SettlementRow key={'s' + en.s.id} s={en.s} />)}
</div> </div>
</div> </div>
) )
@@ -300,11 +325,13 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
<div className={cardCls} style={{ borderRadius: 22, padding: '22px 24px' }}> <div className={cardCls} style={{ borderRadius: 22, padding: '22px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<div className={labelCls}>{t('costs.settleUp')} · <span className="text-content">{(settlement?.flows || []).length}</span></div> <div className={labelCls}>{t('costs.settleUp')} · <span className="text-content">{(settlement?.flows || []).length}</span></div>
<button disabled={!(settlement?.settlements || []).length} onClick={() => setHistOpen(true)} {canEdit && (
className="text-content-muted bg-surface-secondary border border-edge disabled:opacity-40" <button onClick={() => setAddingPayment(true)}
className="text-content-muted bg-surface-secondary border border-edge"
style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 9px', borderRadius: 8, fontSize: 11.5, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}> style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 9px', borderRadius: 8, fontSize: 11.5, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
<History size={13} /> {t('costs.history')}{(settlement?.settlements || []).length ? ` (${settlement!.settlements.length})` : ''} <Plus size={13} /> {t('costs.addPayment')}
</button> </button>
)}
</div> </div>
<SettleFlows /> <SettleFlows />
</div> </div>
@@ -330,9 +357,11 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
onSaved={() => { setModalOpen(false); loadBudgetItems(tripId); loadSettlement() }} /> onSaved={() => { setModalOpen(false); loadBudgetItems(tripId); loadSettlement() }} />
)} )}
<Modal isOpen={histOpen} onClose={() => setHistOpen(false)} title={t('costs.settleHistory')} size="md"> {(editingSettlement || addingPayment) && (
<SettleHistory settlements={settlement?.settlements || []} fmt={fmt} Avatar={Avatar} name={personName} onUndo={undoSettlement} canEdit={canEdit} /> <SettlementModal tripId={tripId} people={people} me={me} editing={editingSettlement}
</Modal> onClose={() => { setEditingSettlement(null); setAddingPayment(false) }}
onSaved={() => { setEditingSettlement(null); setAddingPayment(false); loadSettlement() }} />
)}
<style>{` <style>{`
.costs-root { .costs-root {
@@ -438,7 +467,9 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
<div className={cardCls} style={{ borderRadius: 18, padding: 16 }}> <div className={cardCls} style={{ borderRadius: 18, padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14, gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14, gap: 8 }}>
<div className="text-content" style={{ fontSize: 19, fontWeight: 700, letterSpacing: '-0.02em', display: 'flex', alignItems: 'baseline', gap: 8 }}>{t('costs.settleUp')} <span className="text-content-faint" style={{ fontSize: 12, fontWeight: 500 }}>{(settlement?.flows || []).length}</span></div> <div className="text-content" style={{ fontSize: 19, fontWeight: 700, letterSpacing: '-0.02em', display: 'flex', alignItems: 'baseline', gap: 8 }}>{t('costs.settleUp')} <span className="text-content-faint" style={{ fontSize: 12, fontWeight: 500 }}>{(settlement?.flows || []).length}</span></div>
<button disabled={!(settlement?.settlements || []).length} onClick={() => setHistOpen(true)} className="text-content-muted bg-surface-card border border-edge disabled:opacity-40" style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '6px 10px', borderRadius: 9, fontSize: 11.5, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}><History size={13} /> {t('costs.history')}</button> {canEdit && (
<button onClick={() => setAddingPayment(true)} className="text-content-muted bg-surface-card border border-edge" style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '6px 10px', borderRadius: 9, fontSize: 11.5, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}><Plus size={13} /> {t('costs.addPayment')}</button>
)}
</div> </div>
<SettleFlows /> <SettleFlows />
</div> </div>
@@ -458,11 +489,13 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
{dayGroups.length === 0 {dayGroups.length === 0
? <div className="text-content-faint" style={{ textAlign: 'center', padding: '36px 16px', fontSize: 13 }}>{search ? t('costs.noMatch') : t('costs.emptyText')}</div> ? <div className="text-content-faint" style={{ textAlign: 'center', padding: '36px 16px', fontSize: 13 }}>{search ? t('costs.noMatch') : t('costs.emptyText')}</div>
: dayGroups.map(g => { : dayGroups.map(g => {
const dtot = g.items.reduce((a, e) => a + baseTotal(e), 0) const dtot = g.entries.reduce((a, en) => en.kind === 'expense' ? a + baseTotal(en.e) : a, 0)
return ( return (
<div key={g.day} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div key={g.day} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div className={labelCls} style={{ display: 'flex', alignItems: 'center', padding: '0 2px' }}>{g.day}<span className="text-content-muted" style={{ marginLeft: 'auto', textTransform: 'none', letterSpacing: 0, fontWeight: 500, fontSize: 11.5 }}>{t('costs.spent', { amount: fmt(dtot) })}</span></div> <div className={labelCls} style={{ display: 'flex', alignItems: 'center', padding: '0 2px' }}>{g.day}<span className="text-content-muted" style={{ marginLeft: 'auto', textTransform: 'none', letterSpacing: 0, fontWeight: 500, fontSize: 11.5 }}>{t('costs.spent', { amount: fmt(dtot) })}</span></div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{g.items.map(e => <ExpenseRow key={e.id} e={e} />)}</div> <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{g.entries.map(en => en.kind === 'expense'
? <ExpenseRow key={'e' + en.e.id} e={en.e} />
: <SettlementRow key={'s' + en.s.id} s={en.s} />)}</div>
</div> </div>
) )
})} })}
@@ -490,11 +523,22 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
const cur = curOf(e) const cur = curOf(e)
const payers = (e.payers || []).filter(p => p.amount > 0) const payers = (e.payers || []).filter(p => p.amount > 0)
const net = round2(myPaidOf(e) - myShareOf(e)) const net = round2(myPaidOf(e) - myShareOf(e))
// "Unfinished": a recorded total nobody has paid yet — counts toward the trip
// total but stays out of settlements until who-paid is filled in.
const isUnfinished = baseTotal(e) > 0 && payers.length === 0
return ( return (
<div className="bg-surface-card border border-edge exp-row" style={{ display: 'grid', gridTemplateColumns: '46px 1fr auto', gap: 16, alignItems: 'center', borderRadius: 18, padding: '16px 20px' }}> <div className="bg-surface-card border border-edge exp-row" style={{ display: 'grid', gridTemplateColumns: '46px 1fr auto', gap: 16, alignItems: 'center', borderRadius: 18, padding: '16px 20px' }}>
<span style={{ width: 46, height: 46, borderRadius: 13, display: 'grid', placeItems: 'center', background: c.color + '22', color: c.color }}><Icon size={21} /></span> <span style={{ width: 46, height: 46, borderRadius: 13, display: 'grid', placeItems: 'center', background: c.color + '22', color: c.color }}><Icon size={21} /></span>
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
<div className="text-content" style={{ fontSize: 15, fontWeight: 600, marginBottom: 6 }}>{e.name}</div> <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 6 }}>
<span className="text-content" style={{ fontSize: 15, fontWeight: 600 }}>{e.name}</span>
{isUnfinished && (
<span title={t('costs.unfinishedHint')} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 8px 2px 6px', borderRadius: 999, background: 'rgba(217,119,6,0.14)', color: '#d97706', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>
<span style={{ width: 14, height: 14, borderRadius: '50%', background: '#d97706', color: '#fff', display: 'grid', placeItems: 'center', fontSize: 10, fontWeight: 800 }}>!</span>
{t('costs.unfinished')}
</span>
)}
</div>
{payers.length > 0 && ( {payers.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 5 }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 5 }}>
{payers.map(p => ( {payers.map(p => (
@@ -514,7 +558,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
<div style={{ display: 'flex', alignItems: 'center', gap: 10, alignSelf: 'center' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10, alignSelf: 'center' }}>
<div style={{ textAlign: 'right', whiteSpace: 'nowrap' }}> <div style={{ textAlign: 'right', whiteSpace: 'nowrap' }}>
<div className="text-content" style={{ fontSize: 18, fontWeight: 600 }}>{fmt(baseTotal(e))}</div> <div className="text-content" style={{ fontSize: 18, fontWeight: 600 }}>{fmt(baseTotal(e))}</div>
{(e.members || []).length > 0 && Math.abs(net) > 0.01 && ( {!isUnfinished && (e.members || []).length > 0 && Math.abs(net) > 0.01 && (
<div style={{ fontSize: 12, marginTop: 2, fontWeight: 500, whiteSpace: 'nowrap', color: net > 0 ? '#16a34a' : '#dc2626' }}> <div style={{ fontSize: 12, marginTop: 2, fontWeight: 500, whiteSpace: 'nowrap', color: net > 0 ? '#16a34a' : '#dc2626' }}>
{net > 0 ? t('costs.youLent', { amount: fmt(net) }) : t('costs.youBorrowed', { amount: fmt(-net) })} {net > 0 ? t('costs.youLent', { amount: fmt(net) }) : t('costs.youBorrowed', { amount: fmt(-net) })}
</div> </div>
@@ -531,6 +575,32 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
) )
} }
// A settle-up payment as a ledger row — visually distinct from an expense, with
// inline edit + undo (reuses deleteSettlement) so it isn't buried in a modal.
function SettlementRow({ s }: { s: Settlement }) {
return (
<div className="bg-surface-card border border-edge exp-row" style={{ display: 'grid', gridTemplateColumns: '46px 1fr auto', gap: 16, alignItems: 'center', borderRadius: 18, padding: '16px 20px' }}>
<span style={{ width: 46, height: 46, borderRadius: 13, display: 'grid', placeItems: 'center', background: 'rgba(22,163,74,0.12)', color: '#16a34a' }}><ArrowLeftRight size={21} /></span>
<div style={{ minWidth: 0 }}>
<div className="text-content" style={{ fontSize: 15, fontWeight: 600, marginBottom: 6 }}>{t('costs.payment')}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }} title={`${personName(s.from_user_id)}${personName(s.to_user_id)}`}>
<Avatar id={s.from_user_id} size={20} /><ArrowRight size={13} className="text-content-faint" /><Avatar id={s.to_user_id} size={20} />
<span className="text-content-faint" style={{ fontSize: 12, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{personName(s.from_user_id)} {personName(s.to_user_id)}</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, alignSelf: 'center' }}>
<div className="text-content" style={{ fontSize: 18, fontWeight: 600, whiteSpace: 'nowrap' }}>{fmt(s.amount)}</div>
{canEdit && (
<div className="exp-actions" style={{ display: 'flex', flexDirection: 'column', gap: 6, flexShrink: 0 }}>
<button title={t('common.edit')} onClick={() => setEditingSettlement(s)} className="bg-surface-secondary border border-edge text-content-muted" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 28, height: 28, borderRadius: 999, cursor: 'pointer' }}><Pencil size={13} /></button>
<button title={t('costs.undo')} onClick={() => undoSettlement(s.id)} className="bg-surface-secondary border border-edge" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 28, height: 28, borderRadius: 999, cursor: 'pointer', color: '#dc2626' }}><RotateCcw size={13} /></button>
</div>
)}
</div>
</div>
)
}
function BalancesList({ balances }: { balances: SettlementData['balances'] }) { function BalancesList({ balances }: { balances: SettlementData['balances'] }) {
const rows = people.map(p => balances.find(b => b.user_id === p.id) || { user_id: p.id, username: p.username, avatar_url: null, balance: 0 }) const rows = people.map(p => balances.find(b => b.user_id === p.id) || { user_id: p.id, username: p.username, avatar_url: null, balance: 0 })
const max = Math.max(1, ...rows.map(r => Math.abs(r.balance))) const max = Math.max(1, ...rows.map(r => Math.abs(r.balance)))
@@ -633,31 +703,62 @@ function FlowPills({ ids, lead, Avatar, name }: { ids: number[]; lead: string; A
) )
} }
function SettleHistory({ settlements, fmt, Avatar, name, onUndo, canEdit }: { // Add or edit a settle-up payment (from / to / amount). Reachable inline from the
settlements: Settlement[]; fmt: (v: number) => string; Avatar: (p: { id: number; size?: number }) => React.JSX.Element; name: (id: number) => string; onUndo: (id: number) => void; canEdit: boolean // ledger row and from a manual "Add payment" button, so recording "I sent money to
// X" works the same whether or not there's an outstanding expense behind it.
function SettlementModal({ tripId, people, me, editing, onClose, onSaved }: {
tripId: number; people: TripMember[]; me: number; editing: Settlement | null; onClose: () => void; onSaved: () => void
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
if (settlements.length === 0) return <div className="text-content-faint" style={{ textAlign: 'center', padding: 30, fontSize: 13 }}>{t('costs.noSettlements')}</div> const toast = useToast()
const total = settlements.reduce((a, s) => a + s.amount, 0) const otherDefault = people.find(p => p.id !== me)?.id ?? me
const [fromId, setFromId] = useState<string>(String(editing?.from_user_id ?? me))
const [toId, setToId] = useState<string>(String(editing?.to_user_id ?? otherDefault))
const [amount, setAmount] = useState<string>(editing ? String(editing.amount) : '')
const [saving, setSaving] = useState(false)
const amt = parseFloat(amount) || 0
const valid = amt > 0 && fromId !== toId
const opts = people.map(p => ({ value: String(p.id), label: p.id === me ? t('costs.you') : p.username }))
const save = async () => {
if (!valid) return
setSaving(true)
const data = { from_user_id: Number(fromId), to_user_id: Number(toId), amount: amt }
try {
if (editing) await budgetApi.updateSettlement(tripId, editing.id, data)
else await budgetApi.createSettlement(tripId, data)
onSaved()
} catch { toast.error(t('common.unknownError')) } finally { setSaving(false) }
}
const inputCls = 'w-full bg-surface-input border border-edge text-content'
const labelCls = 'block text-[11px] font-semibold uppercase tracking-[0.08em] text-content-faint mb-[6px]'
return ( return (
<Modal isOpen onClose={onClose} title={editing ? t('costs.editPayment') : t('costs.addPayment')} size="md"
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button onClick={onClose} className="text-content-muted border border-edge" style={{ padding: '8px 16px', borderRadius: 10, background: 'none', fontSize: 13, cursor: 'pointer', fontFamily: 'inherit' }}>{t('common.cancel')}</button>
<button onClick={save} disabled={!valid || saving} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{ padding: '8px 20px', borderRadius: 10, border: 0, fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: !valid || saving ? 0.5 : 1 }}>{editing ? t('common.save') : t('costs.addPayment')}</button>
</div>
}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div> <div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '12px 14px', borderRadius: 12, marginBottom: 14, background: 'rgba(22,163,74,0.1)', color: '#16a34a', fontWeight: 600, fontSize: 13 }}> <label className={labelCls}>{t('costs.from')}</label>
<span>{t('costs.paymentsSettled', { count: settlements.length })}</span><span>{fmt(total)}</span> <CustomSelect value={fromId} onChange={v => setFromId(String(v))} options={opts} style={{ width: '100%' }} />
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div>
{settlements.map(s => ( <label className={labelCls}>{t('costs.to')}</label>
<div key={s.id} className="bg-surface-secondary border border-edge" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '12px 14px', borderRadius: 12 }}> <CustomSelect value={toId} onChange={v => setToId(String(v))} options={opts} style={{ width: '100%' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }} title={`${name(s.from_user_id)}${name(s.to_user_id)}`}>
<Avatar id={s.from_user_id} size={30} /><ArrowRight size={15} className="text-content-faint" /><Avatar id={s.to_user_id} size={30} />
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <div>
<span className="text-content" style={{ fontSize: 14, fontWeight: 600 }}>{fmt(s.amount)}</span> <label className={labelCls}>{t('costs.amount')}</label>
{canEdit && <button onClick={() => onUndo(s.id)} className="bg-surface-card border border-edge text-content-muted" style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '6px 10px', borderRadius: 8, fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}><RotateCcw size={12} /> {t('costs.undo')}</button>} <input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={amount}
</div> onChange={e => setAmount(e.target.value)} className={inputCls} style={{ borderRadius: 10, padding: '11px 13px', fontSize: 14, outline: 'none', fontWeight: 600 }} />
</div>
))}
</div> </div>
</div> </div>
</Modal>
) )
} }
@@ -682,43 +783,88 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo
const [cat, setCat] = useState<string>(editing ? catMeta(editing.category).key : (prefill?.category || 'food')) const [cat, setCat] = useState<string>(editing ? catMeta(editing.category).key : (prefill?.category || 'food'))
const [currency, setCurrency] = useState((editing?.currency || base).toUpperCase()) const [currency, setCurrency] = useState((editing?.currency || base).toUpperCase())
const [day, setDay] = useState(editing?.expense_date || new Date().toISOString().slice(0, 10)) const [day, setDay] = useState(editing?.expense_date || new Date().toISOString().slice(0, 10))
const [payers, setPayers] = useState<Record<number, string>>(() => { // One participant list: a person is "in" the split and may have paid an amount.
const m: Record<number, string> = {} // Entering the total auto-distributes it equally across the non-pinned participants;
for (const p of editing?.payers || []) m[p.user_id] = String(p.amount) // touching an amount pins it and the rest rebalance so the paid amounts always sum
return m // back to the total. Leaving every amount blank = an unfinished expense (counts
}) // toward the trip total only, never settlements, until who-paid is filled in).
// Standalone total for "recorded amount, nobody has paid yet" expenses (created const [total, setTotal] = useState<string>(() => {
// from a booking, or pre-rework items). Used only while no per-person amount is if (editing) return editing.total_price ? String(editing.total_price) : ''
// entered; once a payer has an amount, the total derives from the payers.
const [amount, setAmount] = useState<string>(() => {
if (editing && !(editing.payers && editing.payers.length > 0)) return editing.total_price ? String(editing.total_price) : ''
if (prefill?.amount != null) return String(prefill.amount) if (prefill?.amount != null) return String(prefill.amount)
return '' return ''
}) })
const [split, setSplit] = useState<Set<number>>(() => const [participants, setParticipants] = useState<Set<number>>(() =>
editing ? new Set((editing.members || []).map(m => m.user_id)) : new Set(people.map(p => p.id))) editing ? new Set((editing.members || []).map(m => m.user_id)) : new Set(people.map(p => p.id)))
const [paid, setPaid] = useState<Record<number, string>>(() => {
const m: Record<number, string> = {}
for (const p of editing?.payers || []) if (p.amount > 0) m[p.user_id] = String(p.amount)
return m
})
// Amounts the user pinned by typing — kept out of the auto-rebalance. Existing
// payer amounts load as pinned so opening an expense never reshuffles them.
const [dirty, setDirty] = useState<Set<number>>(() =>
new Set((editing?.payers || []).filter(p => p.amount > 0).map(p => p.user_id)))
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const payersTotal = Object.values(payers).reduce((a, v) => a + (parseFloat(v) || 0), 0) const totalNum = parseFloat(total) || 0
const hasPayers = payersTotal > 0 const paidSum = round2([...participants].reduce((a, id) => a + (parseFloat(paid[id]) || 0), 0))
const total = hasPayers ? payersTotal : (parseFloat(amount) || 0) const paidEntered = paidSum > 0
const each = split.size > 0 ? total / split.size : 0 const balanced = Math.abs(paidSum - totalNum) < 0.01
const valid = name.trim().length > 0 && total > 0 && (hasPayers ? split.size > 0 : true) const each = participants.size > 0 ? totalNum / participants.size : 0
const valid = name.trim().length > 0 && totalNum > 0 && participants.size > 0 && (!paidEntered || balanced)
// Spread `amount` across `n` people in whole cents so the parts sum back exactly.
const splitCents = (amount: number, n: number): number[] => {
if (n <= 0) return []
const cents = Math.max(0, Math.round(amount * 100))
const base = Math.floor(cents / n), rem = cents - base * n
return Array.from({ length: n }, (_, i) => (base + (i < rem ? 1 : 0)) / 100)
}
// Recompute the non-pinned participants so every paid amount sums to the total.
const rebalance = (paidMap: Record<number, string>, dirtySet: Set<number>, parts: Set<number>, totalVal: number): Record<number, string> => {
const ids = [...parts]
const free = ids.filter(id => !dirtySet.has(id))
if (free.length === 0) return paidMap
const pinnedSum = ids.filter(id => dirtySet.has(id)).reduce((a, id) => a + (parseFloat(paidMap[id]) || 0), 0)
const shares = splitCents(totalVal - pinnedSum, free.length)
const next = { ...paidMap }
free.forEach((id, i) => { next[id] = shares[i] ? String(shares[i]) : '' })
return next
}
const onTotalChange = (v: string) => {
setTotal(v)
setPaid(prev => rebalance(prev, dirty, participants, parseFloat(v) || 0))
}
const onPaidChange = (id: number, v: string) => {
const nextDirty = new Set(dirty); nextDirty.add(id)
setDirty(nextDirty)
setPaid(prev => rebalance({ ...prev, [id]: v }, nextDirty, participants, totalNum))
}
const toggleParticipant = (id: number) => {
const nextParts = new Set(participants), nextDirty = new Set(dirty), nextPaid = { ...paid }
if (nextParts.has(id)) { nextParts.delete(id); nextDirty.delete(id); delete nextPaid[id] }
else nextParts.add(id)
setParticipants(nextParts); setDirty(nextDirty)
setPaid(rebalance(nextPaid, nextDirty, nextParts, totalNum))
}
const save = async () => { const save = async () => {
if (!valid) return if (!valid) return
setSaving(true) setSaving(true)
const payerList = Object.entries(payers).map(([uid, v]) => ({ user_id: Number(uid), amount: parseFloat(v) || 0 })).filter(p => p.amount > 0) const payerList = [...participants]
.map(id => ({ user_id: id, amount: parseFloat(paid[id]) || 0 }))
.filter(p => p.amount > 0)
const data = { const data = {
name: name.trim(), category: cat, name: name.trim(), category: cat,
// Store the actual currency the amounts were entered in; conversion to the // Store the actual currency the amounts were entered in; conversion to the
// viewer's display currency happens live (real rates), no manual rate. // viewer's display currency happens live (real rates), no manual rate.
currency, currency,
payers: payerList, member_ids: [...split], payers: payerList, member_ids: [...participants],
expense_date: day || null, expense_date: day || null,
// No per-person amounts: record the typed total directly (the server keeps // Always record the entered total: the server keeps it as-is for an unfinished
// it instead of deriving 0 from the empty payer list). // expense (no payers) and otherwise re-derives it from the payer sum (== total).
...(payerList.length === 0 ? { total_price: parseFloat(amount) || 0 } : {}), total_price: totalNum,
// Link a freshly-created expense to its booking (create-from-booking flow). // Link a freshly-created expense to its booking (create-from-booking flow).
...(!editing && prefill?.reservationId ? { reservation_id: prefill.reservationId } : {}), ...(!editing && prefill?.reservationId ? { reservation_id: prefill.reservationId } : {}),
} }
@@ -750,13 +896,9 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo
<label className={labelCls}>{t('costs.totalAmount')}</label> <label className={labelCls}>{t('costs.totalAmount')}</label>
<div className="bg-surface-input border border-edge" style={{ height: FIELD_H, boxSizing: 'border-box', display: 'flex', alignItems: 'center', borderRadius: 10, padding: '0 12px' }}> <div className="bg-surface-input border border-edge" style={{ height: FIELD_H, boxSizing: 'border-box', display: 'flex', alignItems: 'center', borderRadius: 10, padding: '0 12px' }}>
<span className="text-content-faint" style={{ fontSize: 15 }}>{sym(currency)}</span> <span className="text-content-faint" style={{ fontSize: 15 }}>{sym(currency)}</span>
{hasPayers ? ( <input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={total}
<span className="text-content" style={{ flex: 1, fontSize: 15, fontWeight: 600, paddingLeft: 6 }}>{payersTotal.toFixed(2)}</span> onChange={e => onTotalChange(e.target.value)}
) : (
<input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={amount}
onChange={e => setAmount(e.target.value)}
className="text-content" style={{ flex: 1, border: 0, background: 'none', outline: 'none', fontSize: 15, fontWeight: 600, paddingLeft: 6, width: '100%' }} /> className="text-content" style={{ flex: 1, border: 0, background: 'none', outline: 'none', fontSize: 15, fontWeight: 600, paddingLeft: 6, width: '100%' }} />
)}
</div> </div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
@@ -772,11 +914,11 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo
</div> </div>
</div> </div>
{currency !== base && total > 0 && ( {currency !== base && totalNum > 0 && (
<div className="bg-surface-secondary border border-edge text-content-muted" style={{ borderRadius: 10, padding: '10px 12px', fontSize: 12.5, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}> <div className="bg-surface-secondary border border-edge text-content-muted" style={{ borderRadius: 10, padding: '10px 12px', fontSize: 12.5, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span>{formatMoney(total, currency, locale)}</span> <span>{formatMoney(totalNum, currency, locale)}</span>
<span className="text-content-faint"></span> <span className="text-content-faint"></span>
<span className="text-content" style={{ fontWeight: 600 }}>{formatMoney(convert(total, currency), base, locale)}</span> <span className="text-content" style={{ fontWeight: 600 }}>{formatMoney(convert(totalNum, currency), base, locale)}</span>
<span className="text-content-faint">· {t('costs.liveRate')}</span> <span className="text-content-faint">· {t('costs.liveRate')}</span>
</div> </div>
)} )}
@@ -801,39 +943,37 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo
<div> <div>
<label className={labelCls}>{t('costs.whoPaid')}</label> <label className={labelCls}>{t('costs.whoPaid')}</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
{people.map(p => ( {people.map((p, idx) => {
<div key={p.id} className="bg-surface-secondary border border-edge" style={{ display: 'grid', gridTemplateColumns: '1fr 130px', gap: 10, alignItems: 'center', padding: '8px 11px', borderRadius: 10 }}> const on = participants.has(p.id)
<span className="text-content" style={{ fontSize: 14, fontWeight: 500 }}>{p.id === me ? t('costs.you') : p.username}</span> return (
<div key={p.id} className="bg-surface-secondary border border-edge" style={{ display: 'grid', gridTemplateColumns: '1fr 130px', gap: 10, alignItems: 'center', padding: '8px 11px', borderRadius: 10, opacity: on ? 1 : 0.5 }}>
<button onClick={() => toggleParticipant(p.id)} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', padding: 0, minWidth: 0, textAlign: 'left' }}>
{p.avatar_url
? <img src={p.avatar_url} alt="" style={{ width: 22, height: 22, borderRadius: '50%', objectFit: 'cover', display: 'block', flexShrink: 0, opacity: on ? 1 : 0.45 }} />
: <span style={{ width: 22, height: 22, borderRadius: '50%', background: SPLIT_COLORS[idx % SPLIT_COLORS.length].gradient, color: '#fff', display: 'grid', placeItems: 'center', fontSize: 9, fontWeight: 700, flexShrink: 0, opacity: on ? 1 : 0.45 }}>{(p.id === me ? t('costs.youShort') : p.username.charAt(0)).toUpperCase()}</span>}
<span className="text-content" style={{ fontSize: 14, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.id === me ? t('costs.you') : p.username}</span>
</button>
{on ? (
<div className="bg-surface-input border border-edge" style={{ display: 'flex', alignItems: 'center', gap: 4, borderRadius: 8, padding: '0 10px' }}> <div className="bg-surface-input border border-edge" style={{ display: 'flex', alignItems: 'center', gap: 4, borderRadius: 8, padding: '0 10px' }}>
<span className="text-content-faint" style={{ fontSize: 13 }}>{sym(currency)}</span> <span className="text-content-faint" style={{ fontSize: 13 }}>{sym(currency)}</span>
<input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={payers[p.id] || ''} <input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={paid[p.id] || ''}
onChange={e => setPayers(prev => ({ ...prev, [p.id]: e.target.value }))} onChange={e => onPaidChange(p.id, e.target.value)}
className="text-content" style={{ width: '100%', border: 0, background: 'none', outline: 'none', fontSize: 14, fontWeight: 600, padding: '8px 0', textAlign: 'right' }} /> className="text-content" style={{ width: '100%', border: 0, background: 'none', outline: 'none', fontSize: 14, fontWeight: 600, padding: '8px 0', textAlign: 'right' }} />
</div> </div>
) : (
<button onClick={() => toggleParticipant(p.id)} className="text-content-faint" style={{ background: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, textAlign: 'right' }}>{t('costs.tapToInclude')}</button>
)}
</div> </div>
))}
</div>
</div>
<div>
<label className={labelCls}>{t('costs.splitBetween')}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
{people.map(p => {
const on = split.has(p.id)
return (
<button key={p.id} onClick={() => setSplit(prev => { const n = new Set(prev); n.has(p.id) ? n.delete(p.id) : n.add(p.id); return n })}
className={on ? 'bg-surface-card text-content border' : 'bg-surface-secondary text-content-faint border border-edge'}
style={{ display: 'inline-flex', alignItems: 'center', gap: 7, padding: '6px 13px 6px 7px', borderRadius: 999, fontSize: 13, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', borderColor: on ? 'var(--text-primary)' : undefined }}>
{p.avatar_url
? <img src={p.avatar_url} alt="" style={{ width: 22, height: 22, borderRadius: '50%', objectFit: 'cover', display: 'block', opacity: on ? 1 : 0.45 }} />
: <span style={{ width: 22, height: 22, borderRadius: '50%', background: SPLIT_COLORS[people.findIndex(x => x.id === p.id) % SPLIT_COLORS.length].gradient, color: '#fff', display: 'grid', placeItems: 'center', fontSize: 9, fontWeight: 700, opacity: on ? 1 : 0.45 }}>{(p.id === me ? t('costs.youShort') : p.username.charAt(0)).toUpperCase()}</span>}
{p.id === me ? t('costs.you') : p.username}
</button>
) )
})} })}
</div> </div>
<div className="text-content-faint" style={{ marginTop: 10, fontSize: 12.5 }}> <div style={{ marginTop: 10, fontSize: 12.5, display: 'flex', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
{split.size === 0 ? t('costs.pickSomeone') : t('costs.splitSummary', { count: split.size, amount: sym(currency) + each.toFixed(2) })} <span className="text-content-faint">
{participants.size === 0 ? t('costs.pickSomeone') : t('costs.splitSummary', { count: participants.size, amount: sym(currency) + each.toFixed(2) })}
</span>
{paidEntered
? <span style={{ fontWeight: 600, color: balanced ? '#16a34a' : '#dc2626' }}>{sym(currency)}{paidSum.toFixed(2)} / {sym(currency)}{totalNum.toFixed(2)}</span>
: (totalNum > 0 && <span style={{ color: '#d97706', fontWeight: 600 }}>{t('costs.unfinishedHint')}</span>)}
</div> </div>
</div> </div>
</div> </div>
@@ -94,6 +94,31 @@ export class BudgetController {
return { settlement }; return { settlement };
} }
@Put('settlements/:settlementId')
updateSettlement(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('settlementId') settlementId: string,
@Body() body: { from_user_id?: number; to_user_id?: number; amount?: number },
@Headers('x-socket-id') socketId?: string,
) {
const trip = this.requireTrip(tripId, user);
this.requireEdit(trip, user);
if (body.from_user_id == null || body.to_user_id == null || body.amount == null) {
throw new HttpException({ error: 'from_user_id, to_user_id and amount are required' }, 400);
}
const settlement = this.budget.updateSettlement(settlementId, tripId, {
from_user_id: body.from_user_id,
to_user_id: body.to_user_id,
amount: body.amount,
});
if (!settlement) {
throw new HttpException({ error: 'Settlement not found' }, 404);
}
this.budget.broadcast(tripId, 'budget:settlement-updated', { settlement }, socketId);
return { settlement };
}
@Delete('settlements/:settlementId') @Delete('settlements/:settlementId')
deleteSettlement( deleteSettlement(
@CurrentUser() user: User, @CurrentUser() user: User,
+4
View File
@@ -73,6 +73,10 @@ export class BudgetService {
return svc.createSettlement(tripId, data, userId); return svc.createSettlement(tripId, data, userId);
} }
updateSettlement(id: string, tripId: string, data: { from_user_id: number; to_user_id: number; amount: number }) {
return svc.updateSettlement(id, tripId, data);
}
deleteSettlement(id: string, tripId: string): boolean { deleteSettlement(id: string, tripId: string): boolean {
return svc.deleteSettlement(id, tripId); return svc.deleteSettlement(id, tripId);
} }
+23 -3
View File
@@ -385,11 +385,18 @@ export function calculateSettlement(
} }
// Persisted settle-up transfers already moved money: the payer's debt shrinks, // Persisted settle-up transfers already moved money: the payer's debt shrinks,
// the receiver's credit shrinks, so the corresponding flow disappears. // the receiver's credit shrinks, so the corresponding flow disappears. A transfer
// counts even when neither user has an expense-derived balance yet — a manual
// payment, or one left behind after its expense was deleted, then correctly
// surfaces as an amount still to square up instead of silently vanishing.
const settlements = listSettlements(tripId); const settlements = listSettlements(tripId);
const ensureSettled = (id: number, username: string | undefined, avatar_url: string | null | undefined) => {
if (!balances[id]) balances[id] = { user_id: id, username: username || '', avatar_url: avatar_url ?? null, balance: 0 };
return balances[id];
};
for (const s of settlements) { for (const s of settlements) {
if (balances[s.from_user_id]) balances[s.from_user_id].balance += s.amount; ensureSettled(s.from_user_id, s.from_username, s.from_avatar_url).balance += s.amount;
if (balances[s.to_user_id]) balances[s.to_user_id].balance -= s.amount; ensureSettled(s.to_user_id, s.to_username, s.to_avatar_url).balance -= s.amount;
} }
// Calculate optimized payment flows (greedy algorithm) // Calculate optimized payment flows (greedy algorithm)
@@ -461,6 +468,19 @@ export function createSettlement(
return listSettlements(tripId).find(s => s.id === Number(result.lastInsertRowid)) || null; return listSettlements(tripId).find(s => s.id === Number(result.lastInsertRowid)) || null;
} }
export function updateSettlement(
id: string | number,
tripId: string | number,
data: { from_user_id: number; to_user_id: number; amount: number },
) {
const row = db.prepare('SELECT id FROM budget_settlements WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!row) return null;
db.prepare(
'UPDATE budget_settlements SET from_user_id = ?, to_user_id = ?, amount = ? WHERE id = ?'
).run(data.from_user_id, data.to_user_id, Math.round(data.amount * 100) / 100, id);
return listSettlements(tripId).find(s => s.id === Number(id)) || null;
}
export function deleteSettlement(id: string | number, tripId: string | number): boolean { export function deleteSettlement(id: string | number, tripId: string | number): boolean {
const row = db.prepare('SELECT id FROM budget_settlements WHERE id = ? AND trip_id = ?').get(id, tripId); const row = db.prepare('SELECT id FROM budget_settlements WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!row) return false; if (!row) return false;
+15
View File
@@ -31,6 +31,7 @@ const { svc } = vi.hoisted(() => ({
verifyTripAccess: vi.fn(), listBudgetItems: vi.fn(), createBudgetItem: vi.fn(), updateBudgetItem: vi.fn(), verifyTripAccess: vi.fn(), listBudgetItems: vi.fn(), createBudgetItem: vi.fn(), updateBudgetItem: vi.fn(),
deleteBudgetItem: vi.fn(), updateMembers: vi.fn(), toggleMemberPaid: vi.fn(), getPerPersonSummary: vi.fn(), deleteBudgetItem: vi.fn(), updateMembers: vi.fn(), toggleMemberPaid: vi.fn(), getPerPersonSummary: vi.fn(),
calculateSettlement: vi.fn(), reorderBudgetItems: vi.fn(), reorderBudgetCategories: vi.fn(), calculateSettlement: vi.fn(), reorderBudgetItems: vi.fn(), reorderBudgetCategories: vi.fn(),
setItemPayers: vi.fn(), listSettlements: vi.fn(), createSettlement: vi.fn(), updateSettlement: vi.fn(), deleteSettlement: vi.fn(),
}, },
})); }));
vi.mock('../../src/services/budgetService', () => svc); vi.mock('../../src/services/budgetService', () => svc);
@@ -104,4 +105,18 @@ describe('Budget e2e (real auth guard + temp SQLite)', () => {
expect(res.status).toBe(400); expect(res.status).toBe(400);
expect(res.body).toEqual({ error: 'user_ids must be an array' }); expect(res.body).toEqual({ error: 'user_ids must be an array' });
}); });
it('200 on settlement update with permission', async () => {
svc.updateSettlement.mockReturnValue({ id: 7, from_user_id: 2, to_user_id: 1, amount: 15 });
const res = await request(server).put('/api/trips/5/budget/settlements/7').set('Cookie', sessionCookie(1)).send({ from_user_id: 2, to_user_id: 1, amount: 15 });
expect(res.status).toBe(200);
expect(res.body).toEqual({ settlement: { id: 7, from_user_id: 2, to_user_id: 1, amount: 15 } });
});
it('404 on settlement update when it does not exist', async () => {
svc.updateSettlement.mockReturnValue(null);
const res = await request(server).put('/api/trips/5/budget/settlements/7').set('Cookie', sessionCookie(1)).send({ from_user_id: 2, to_user_id: 1, amount: 15 });
expect(res.status).toBe(404);
expect(res.body).toEqual({ error: 'Settlement not found' });
});
}); });
@@ -111,6 +111,37 @@ describe('BudgetController (parity with the legacy /api/trips/:tripId/budget rou
expect(new BudgetController(svc).deleteSettlement(user, '5', '7', 'sock')).toEqual({ success: true }); expect(new BudgetController(svc).deleteSettlement(user, '5', '7', 'sock')).toEqual({ success: true });
expect(broadcast).toHaveBeenCalledWith('5', 'budget:settlement-deleted', { settlementId: 7 }, 'sock'); expect(broadcast).toHaveBeenCalledWith('5', 'budget:settlement-deleted', { settlementId: 7 }, 'sock');
}); });
it('PUT /settlements/:id 403 without budget_edit', () => {
const svc = makeService({ canEdit: vi.fn().mockReturnValue(false) });
expect(thrown(() => new BudgetController(svc).updateSettlement(user, '5', '7', { from_user_id: 1, to_user_id: 2, amount: 10 }))).toEqual({
status: 403, body: { error: 'No permission' },
});
});
it('PUT /settlements/:id 400 when a field is missing', () => {
const svc = makeService();
expect(thrown(() => new BudgetController(svc).updateSettlement(user, '5', '7', { from_user_id: 1, to_user_id: 2 }))).toEqual({
status: 400, body: { error: 'from_user_id, to_user_id and amount are required' },
});
});
it('PUT /settlements/:id 404 when missing', () => {
const svc = makeService({ updateSettlement: vi.fn().mockReturnValue(null) } as Partial<BudgetService>);
expect(thrown(() => new BudgetController(svc).updateSettlement(user, '5', '7', { from_user_id: 1, to_user_id: 2, amount: 10 }))).toEqual({
status: 404, body: { error: 'Settlement not found' },
});
});
it('PUT /settlements/:id updates and broadcasts', () => {
const updateSettlement = vi.fn().mockReturnValue({ id: 7, from_user_id: 2, to_user_id: 1, amount: 15 });
const broadcast = vi.fn();
const svc = makeService({ updateSettlement, broadcast } as Partial<BudgetService>);
const res = new BudgetController(svc).updateSettlement(user, '5', '7', { from_user_id: 2, to_user_id: 1, amount: 15 }, 'sock');
expect(res).toEqual({ settlement: { id: 7, from_user_id: 2, to_user_id: 1, amount: 15 } });
expect(updateSettlement).toHaveBeenCalledWith('7', '5', { from_user_id: 2, to_user_id: 1, amount: 15 });
expect(broadcast).toHaveBeenCalledWith('5', 'budget:settlement-updated', { settlement: { id: 7, from_user_id: 2, to_user_id: 1, amount: 15 } }, 'sock');
});
}); });
describe('POST /', () => { describe('POST /', () => {
@@ -17,7 +17,7 @@ const mockDb = vi.hoisted(() => {
vi.mock('../../../src/db/database', () => mockDb); vi.mock('../../../src/db/database', () => mockDb);
import { calculateSettlement } from '../../../src/services/budgetService'; import { calculateSettlement, updateSettlement } from '../../../src/services/budgetService';
import type { BudgetItem, BudgetItemMember, BudgetItemPayer } from '../../../src/types'; import type { BudgetItem, BudgetItemMember, BudgetItemPayer } from '../../../src/types';
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -189,4 +189,60 @@ describe('calculateSettlement', () => {
expect(result.flows).toHaveLength(1); expect(result.flows).toHaveLength(1);
expect(result.flows[0].amount).toBe(20); expect(result.flows[0].amount).toBe(20);
}); });
it('counts a settlement with no matching expense as an amount still to square up', () => {
// bob paid alice 30 but every expense behind it was deleted: alice now owes bob.
mockDb.db.prepare.mockImplementation((sql: string) => {
if (sql.includes('FROM budget_settlements')) {
return { all: vi.fn(() => [
{ id: 1, trip_id: 1, from_user_id: 2, to_user_id: 1, amount: 30, from_username: 'bob', to_username: 'alice', from_avatar: null, to_avatar: null },
]), get: vi.fn(), run: vi.fn() };
}
return { all: vi.fn(() => []), get: vi.fn(), run: vi.fn() };
});
const result = calculateSettlement(1);
const alice = result.balances.find(b => b.user_id === 1)!;
const bob = result.balances.find(b => b.user_id === 2)!;
expect(bob.balance).toBe(30);
expect(alice.balance).toBe(-30);
expect(result.flows).toEqual([
expect.objectContaining({ amount: 30, from: expect.objectContaining({ user_id: 1 }), to: expect.objectContaining({ user_id: 2 }) }),
]);
});
});
// ── updateSettlement ──────────────────────────────────────────────────────────
describe('updateSettlement', () => {
it('returns null when the settlement is not in the trip', () => {
mockDb.db.prepare.mockImplementation((sql: string) => {
if (sql.includes('SELECT id FROM budget_settlements')) {
return { get: vi.fn(() => undefined), all: vi.fn(), run: vi.fn() };
}
return { get: vi.fn(), all: vi.fn(() => []), run: vi.fn() };
});
expect(updateSettlement(7, 1, { from_user_id: 2, to_user_id: 1, amount: 10 })).toBeNull();
});
it('updates the row (rounded to cents) and returns the refreshed settlement', () => {
const run = vi.fn();
mockDb.db.prepare.mockImplementation((sql: string) => {
if (sql.includes('SELECT id FROM budget_settlements')) {
return { get: vi.fn(() => ({ id: 7 })), all: vi.fn(), run: vi.fn() };
}
if (sql.includes('UPDATE budget_settlements')) {
return { get: vi.fn(), all: vi.fn(), run };
}
if (sql.includes('FROM budget_settlements')) {
return { get: vi.fn(), all: vi.fn(() => [
{ id: 7, trip_id: 1, from_user_id: 2, to_user_id: 1, amount: 10.13, from_username: 'bob', to_username: 'alice', from_avatar: null, to_avatar: null },
]), run: vi.fn() };
}
return { get: vi.fn(), all: vi.fn(() => []), run: vi.fn() };
});
const res = updateSettlement(7, 1, { from_user_id: 2, to_user_id: 1, amount: 10.126 });
expect(run).toHaveBeenCalledWith(2, 1, 10.13, 7);
expect(res).toMatchObject({ id: 7, from_user_id: 2, to_user_id: 1, amount: 10.13 });
});
}); });
+1
View File
@@ -1,4 +1,5 @@
{ {
"printWidth": 120,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"plugins": [ "plugins": [
+6 -19
View File
@@ -16,16 +16,9 @@ describe('adminUserCreateRequestSchema', () => {
role: 'admin', role: 'admin',
}).success, }).success,
).toBe(true); ).toBe(true);
expect( expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true);
adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success, expect(adminUserCreateRequestSchema.safeParse({ password: 'p' }).success).toBe(false);
).toBe(true); expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', role: 'root' }).success).toBe(false);
expect(
adminUserCreateRequestSchema.safeParse({ password: 'p' }).success,
).toBe(false);
expect(
adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', role: 'root' })
.success,
).toBe(false);
}); });
}); });
@@ -49,19 +42,13 @@ describe('adminInviteCreateRequestSchema', () => {
}).success, }).success,
).toBe(true); ).toBe(true);
expect(adminInviteCreateRequestSchema.safeParse({}).success).toBe(true); expect(adminInviteCreateRequestSchema.safeParse({}).success).toBe(true);
expect( expect(adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success).toBe(false);
adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success,
).toBe(false);
}); });
}); });
describe('adminFeatureToggleRequestSchema', () => { describe('adminFeatureToggleRequestSchema', () => {
it('requires a boolean enabled', () => { it('requires a boolean enabled', () => {
expect( expect(adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success).toBe(true);
adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success, expect(adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false);
).toBe(true);
expect(
adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success,
).toBe(false);
}); });
}); });
+4 -12
View File
@@ -14,29 +14,21 @@ export const adminUserCreateRequestSchema = z.object({
username: z.string().optional(), username: z.string().optional(),
role: z.enum(['user', 'admin']).optional(), role: z.enum(['user', 'admin']).optional(),
}); });
export type AdminUserCreateRequest = z.infer< export type AdminUserCreateRequest = z.infer<typeof adminUserCreateRequestSchema>;
typeof adminUserCreateRequestSchema
>;
export const adminPermissionsRequestSchema = z.object({ export const adminPermissionsRequestSchema = z.object({
permissions: z.record(z.string(), z.unknown()), permissions: z.record(z.string(), z.unknown()),
}); });
export type AdminPermissionsRequest = z.infer< export type AdminPermissionsRequest = z.infer<typeof adminPermissionsRequestSchema>;
typeof adminPermissionsRequestSchema
>;
export const adminInviteCreateRequestSchema = z.object({ export const adminInviteCreateRequestSchema = z.object({
max_uses: z.number().optional(), max_uses: z.number().optional(),
expires_in_days: z.number().optional(), expires_in_days: z.number().optional(),
role: z.enum(['user', 'admin']).optional(), role: z.enum(['user', 'admin']).optional(),
}); });
export type AdminInviteCreateRequest = z.infer< export type AdminInviteCreateRequest = z.infer<typeof adminInviteCreateRequestSchema>;
typeof adminInviteCreateRequestSchema
>;
export const adminFeatureToggleRequestSchema = z.object({ export const adminFeatureToggleRequestSchema = z.object({
enabled: z.boolean(), enabled: z.boolean(),
}); });
export type AdminFeatureToggleRequest = z.infer< export type AdminFeatureToggleRequest = z.infer<typeof adminFeatureToggleRequestSchema>;
typeof adminFeatureToggleRequestSchema
>;
@@ -8,38 +8,23 @@ import { describe, it, expect } from 'vitest';
describe('assignmentCreateRequestSchema', () => { describe('assignmentCreateRequestSchema', () => {
it('requires a place_id; notes optional/nullable', () => { it('requires a place_id; notes optional/nullable', () => {
expect( expect(assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(true);
assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success, expect(assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null }).success).toBe(true);
).toBe(true);
expect(
assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null })
.success,
).toBe(true);
expect(assignmentCreateRequestSchema.safeParse({}).success).toBe(false); expect(assignmentCreateRequestSchema.safeParse({}).success).toBe(false);
}); });
}); });
describe('assignmentMoveRequestSchema', () => { describe('assignmentMoveRequestSchema', () => {
it('requires new_day_id; order_index optional', () => { it('requires new_day_id; order_index optional', () => {
expect( expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success).toBe(true);
assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success, expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 }).success).toBe(true);
).toBe(true);
expect(
assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 })
.success,
).toBe(true);
expect(assignmentMoveRequestSchema.safeParse({}).success).toBe(false); expect(assignmentMoveRequestSchema.safeParse({}).success).toBe(false);
}); });
}); });
describe('assignmentParticipantsRequestSchema', () => { describe('assignmentParticipantsRequestSchema', () => {
it('requires a numeric user_ids array', () => { it('requires a numeric user_ids array', () => {
expect( expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] }) expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
.success,
).toBe(true);
expect(
assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success,
).toBe(false);
}); });
}); });
+3 -9
View File
@@ -49,16 +49,12 @@ export const assignmentCreateRequestSchema = z.object({
place_id: z.union([z.number(), z.string()]), place_id: z.union([z.number(), z.string()]),
notes: z.string().nullable().optional(), notes: z.string().nullable().optional(),
}); });
export type AssignmentCreateRequest = z.infer< export type AssignmentCreateRequest = z.infer<typeof assignmentCreateRequestSchema>;
typeof assignmentCreateRequestSchema
>;
export const assignmentReorderRequestSchema = z.object({ export const assignmentReorderRequestSchema = z.object({
orderedIds: z.array(z.number()), orderedIds: z.array(z.number()),
}); });
export type AssignmentReorderRequest = z.infer< export type AssignmentReorderRequest = z.infer<typeof assignmentReorderRequestSchema>;
typeof assignmentReorderRequestSchema
>;
export const assignmentMoveRequestSchema = z.object({ export const assignmentMoveRequestSchema = z.object({
new_day_id: z.union([z.number(), z.string()]), new_day_id: z.union([z.number(), z.string()]),
@@ -75,6 +71,4 @@ export type AssignmentTimeRequest = z.infer<typeof assignmentTimeRequestSchema>;
export const assignmentParticipantsRequestSchema = z.object({ export const assignmentParticipantsRequestSchema = z.object({
user_ids: z.array(z.number()), user_ids: z.array(z.number()),
}); });
export type AssignmentParticipantsRequest = z.infer< export type AssignmentParticipantsRequest = z.infer<typeof assignmentParticipantsRequestSchema>;
typeof assignmentParticipantsRequestSchema
>;
+6 -22
View File
@@ -1,28 +1,17 @@
import { import { markRegionRequestSchema, createBucketItemRequestSchema, regionGeoSchema } from './atlas.schema';
markRegionRequestSchema,
createBucketItemRequestSchema,
regionGeoSchema,
} from './atlas.schema';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
describe('markRegionRequestSchema', () => { describe('markRegionRequestSchema', () => {
it('requires both name and country_code', () => { it('requires both name and country_code', () => {
expect( expect(markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' }).success).toBe(true);
markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' }) expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(false);
.success,
).toBe(true);
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(
false,
);
}); });
}); });
describe('createBucketItemRequestSchema', () => { describe('createBucketItemRequestSchema', () => {
it('requires a name; coordinates and metadata optional/nullable', () => { it('requires a name; coordinates and metadata optional/nullable', () => {
expect( expect(createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success).toBe(true);
createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success,
).toBe(true);
expect( expect(
createBucketItemRequestSchema.safeParse({ createBucketItemRequestSchema.safeParse({
name: 'Tokyo', name: 'Tokyo',
@@ -37,18 +26,13 @@ describe('createBucketItemRequestSchema', () => {
describe('regionGeoSchema', () => { describe('regionGeoSchema', () => {
it('accepts a FeatureCollection with opaque features', () => { it('accepts a FeatureCollection with opaque features', () => {
expect( expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] }).success).toBe(true);
regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] })
.success,
).toBe(true);
expect( expect(
regionGeoSchema.safeParse({ regionGeoSchema.safeParse({
type: 'FeatureCollection', type: 'FeatureCollection',
features: [{ anything: true }], features: [{ anything: true }],
}).success, }).success,
).toBe(true); ).toBe(true);
expect( expect(regionGeoSchema.safeParse({ type: 'Other', features: [] }).success).toBe(false);
regionGeoSchema.safeParse({ type: 'Other', features: [] }).success,
).toBe(false);
}); });
}); });
+163 -27
View File
@@ -29,9 +29,7 @@ export const createBucketItemRequestSchema = z.object({
notes: z.string().nullable().optional(), notes: z.string().nullable().optional(),
target_date: z.string().nullable().optional(), target_date: z.string().nullable().optional(),
}); });
export type CreateBucketItemRequest = z.infer< export type CreateBucketItemRequest = z.infer<typeof createBucketItemRequestSchema>;
typeof createBucketItemRequestSchema
>;
export const updateBucketItemRequestSchema = z.object({ export const updateBucketItemRequestSchema = z.object({
name: z.string().optional(), name: z.string().optional(),
@@ -41,9 +39,7 @@ export const updateBucketItemRequestSchema = z.object({
country_code: z.string().nullable().optional(), country_code: z.string().nullable().optional(),
target_date: z.string().nullable().optional(), target_date: z.string().nullable().optional(),
}); });
export type UpdateBucketItemRequest = z.infer< export type UpdateBucketItemRequest = z.infer<typeof updateBucketItemRequestSchema>;
typeof updateBucketItemRequestSchema
>;
/** A bucket-list item row (DB-shaped; kept open). */ /** A bucket-list item row (DB-shaped; kept open). */
export const bucketItemSchema = open; export const bucketItemSchema = open;
@@ -66,27 +62,167 @@ export type RegionGeo = z.infer<typeof regionGeoSchema>;
* client (keeping the per-continent counts in sync on optimistic mark/unmark). * client (keeping the per-continent counts in sync on optimistic mark/unmark).
*/ */
export const CONTINENT_MAP: Record<string, string> = { export const CONTINENT_MAP: Record<string, string> = {
AF:'Asia',AL:'Europe',DZ:'Africa',AD:'Europe',AO:'Africa',AR:'South America',AM:'Asia',AU:'Oceania',AT:'Europe',AZ:'Asia', AF: 'Asia',
BA:'Europe',BD:'Asia',BF:'Africa',BH:'Asia',BI:'Africa',BJ:'Africa',BN:'Asia',BO:'South America', AL: 'Europe',
BR:'South America',BE:'Europe',BG:'Europe',BW:'Africa', DZ: 'Africa',
CA:'North America',CD:'Africa',CG:'Africa',CI:'Africa',CL:'South America',CM:'Africa',CN:'Asia',CO:'South America', AD: 'Europe',
CR:'North America',CU:'North America',CV:'Africa',CY:'Europe',HR:'Europe',CZ:'Europe', AO: 'Africa',
DJ:'Africa',DK:'Europe',DO:'North America',EC:'South America',EG:'Africa',EE:'Europe',ER:'Africa',ET:'Africa', AR: 'South America',
FI:'Europe',FR:'Europe',DE:'Europe',GE:'Asia',GH:'Africa',GN:'Africa',GR:'Europe',GT:'North America', AM: 'Asia',
HN:'North America',HT:'North America',HU:'Europe',IS:'Europe',IN:'Asia',ID:'Asia',IR:'Asia',IQ:'Asia', AU: 'Oceania',
IE:'Europe',IL:'Asia',IT:'Europe',JM:'North America',JO:'Asia',JP:'Asia',KE:'Africa',KG:'Asia',KH:'Asia', AT: 'Europe',
KR:'Asia',KW:'Asia',KZ:'Asia',LA:'Asia',LB:'Asia',LK:'Asia',LV:'Europe',LT:'Europe',LU:'Europe',LY:'Africa', AZ: 'Asia',
MA:'Africa',MD:'Europe',ME:'Europe',MG:'Africa',MK:'Europe',ML:'Africa',MM:'Asia',MN:'Asia',MR:'Africa', BA: 'Europe',
MT:'Europe',MU:'Africa',MV:'Asia',MW:'Africa',MY:'Asia',MX:'North America',MZ:'Africa', BD: 'Asia',
NA:'Africa',NE:'Africa',NI:'North America',NL:'Europe',NP:'Asia',NZ:'Oceania',NO:'Europe',OM:'Asia', BF: 'Africa',
PA:'North America',PG:'Oceania',PK:'Asia',PE:'South America',PH:'Asia',PL:'Europe',PS:'Asia', BH: 'Asia',
PT:'Europe',PY:'South America',QA:'Asia',RO:'Europe',RU:'Europe',RW:'Africa',SA:'Asia',SC:'Africa', BI: 'Africa',
SD:'Africa',SG:'Asia',SI:'Europe',SK:'Europe',SN:'Africa',SO:'Africa',RS:'Europe',SV:'North America', BJ: 'Africa',
SY:'Asia',TG:'Africa',TJ:'Asia',TM:'Asia',TN:'Africa',TT:'North America',TW:'Asia',TZ:'Africa', BN: 'Asia',
ZA:'Africa',SE:'Europe',CH:'Europe',TH:'Asia',TR:'Europe',UA:'Europe',UG:'Africa',UY:'South America', BO: 'South America',
UZ:'Asia',VE:'South America',AE:'Asia',GB:'Europe',US:'North America',VN:'Asia',XK:'Europe', BR: 'South America',
YE:'Asia',ZM:'Africa',ZW:'Africa',NG:'Africa', BE: 'Europe',
HK:'Asia',MO:'Asia',SM:'Europe',VA:'Europe',MC:'Europe',LI:'Europe',GI:'Europe',PR:'North America', BG: 'Europe',
BW: 'Africa',
CA: 'North America',
CD: 'Africa',
CG: 'Africa',
CI: 'Africa',
CL: 'South America',
CM: 'Africa',
CN: 'Asia',
CO: 'South America',
CR: 'North America',
CU: 'North America',
CV: 'Africa',
CY: 'Europe',
HR: 'Europe',
CZ: 'Europe',
DJ: 'Africa',
DK: 'Europe',
DO: 'North America',
EC: 'South America',
EG: 'Africa',
EE: 'Europe',
ER: 'Africa',
ET: 'Africa',
FI: 'Europe',
FR: 'Europe',
DE: 'Europe',
GE: 'Asia',
GH: 'Africa',
GN: 'Africa',
GR: 'Europe',
GT: 'North America',
HN: 'North America',
HT: 'North America',
HU: 'Europe',
IS: 'Europe',
IN: 'Asia',
ID: 'Asia',
IR: 'Asia',
IQ: 'Asia',
IE: 'Europe',
IL: 'Asia',
IT: 'Europe',
JM: 'North America',
JO: 'Asia',
JP: 'Asia',
KE: 'Africa',
KG: 'Asia',
KH: 'Asia',
KR: 'Asia',
KW: 'Asia',
KZ: 'Asia',
LA: 'Asia',
LB: 'Asia',
LK: 'Asia',
LV: 'Europe',
LT: 'Europe',
LU: 'Europe',
LY: 'Africa',
MA: 'Africa',
MD: 'Europe',
ME: 'Europe',
MG: 'Africa',
MK: 'Europe',
ML: 'Africa',
MM: 'Asia',
MN: 'Asia',
MR: 'Africa',
MT: 'Europe',
MU: 'Africa',
MV: 'Asia',
MW: 'Africa',
MY: 'Asia',
MX: 'North America',
MZ: 'Africa',
NA: 'Africa',
NE: 'Africa',
NI: 'North America',
NL: 'Europe',
NP: 'Asia',
NZ: 'Oceania',
NO: 'Europe',
OM: 'Asia',
PA: 'North America',
PG: 'Oceania',
PK: 'Asia',
PE: 'South America',
PH: 'Asia',
PL: 'Europe',
PS: 'Asia',
PT: 'Europe',
PY: 'South America',
QA: 'Asia',
RO: 'Europe',
RU: 'Europe',
RW: 'Africa',
SA: 'Asia',
SC: 'Africa',
SD: 'Africa',
SG: 'Asia',
SI: 'Europe',
SK: 'Europe',
SN: 'Africa',
SO: 'Africa',
RS: 'Europe',
SV: 'North America',
SY: 'Asia',
TG: 'Africa',
TJ: 'Asia',
TM: 'Asia',
TN: 'Africa',
TT: 'North America',
TW: 'Asia',
TZ: 'Africa',
ZA: 'Africa',
SE: 'Europe',
CH: 'Europe',
TH: 'Asia',
TR: 'Europe',
UA: 'Europe',
UG: 'Africa',
UY: 'South America',
UZ: 'Asia',
VE: 'South America',
AE: 'Asia',
GB: 'Europe',
US: 'North America',
VN: 'Asia',
XK: 'Europe',
YE: 'Asia',
ZM: 'Africa',
ZW: 'Africa',
NG: 'Africa',
HK: 'Asia',
MO: 'Asia',
SM: 'Europe',
VA: 'Europe',
MC: 'Europe',
LI: 'Europe',
GI: 'Europe',
PR: 'North America',
}; };
/** Continent for an ISO alpha-2 country code; 'Other' when unknown. */ /** Continent for an ISO alpha-2 country code; 'Other' when unknown. */
+12 -39
View File
@@ -13,10 +13,7 @@ import { describe, it, expect } from 'vitest';
describe('registerRequestSchema', () => { describe('registerRequestSchema', () => {
it('requires email + password; username/invite optional', () => { it('requires email + password; username/invite optional', () => {
expect( expect(registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true);
registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' })
.success,
).toBe(true);
expect( expect(
registerRequestSchema.safeParse({ registerRequestSchema.safeParse({
email: 'a@b.c', email: 'a@b.c',
@@ -24,32 +21,21 @@ describe('registerRequestSchema', () => {
invite_token: 't', invite_token: 't',
}).success, }).success,
).toBe(true); ).toBe(true);
expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe( expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false);
false,
);
}); });
}); });
describe('loginRequestSchema', () => { describe('loginRequestSchema', () => {
it('requires email + password', () => { it('requires email + password', () => {
expect( expect(loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true);
loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success, expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false);
).toBe(true);
expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(
false,
);
}); });
}); });
describe('forgot/reset/change password schemas', () => { describe('forgot/reset/change password schemas', () => {
it('validate their required fields', () => { it('validate their required fields', () => {
expect( expect(forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true);
forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success, expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' }).success).toBe(true);
).toBe(true);
expect(
resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' })
.success,
).toBe(true);
expect( expect(
resetPasswordRequestSchema.safeParse({ resetPasswordRequestSchema.safeParse({
token: 't', token: 't',
@@ -57,36 +43,23 @@ describe('forgot/reset/change password schemas', () => {
mfa_code: '123456', mfa_code: '123456',
}).success, }).success,
).toBe(true); ).toBe(true);
expect( expect(resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success).toBe(false);
resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success,
).toBe(false);
expect( expect(
changePasswordRequestSchema.safeParse({ changePasswordRequestSchema.safeParse({
current_password: 'a', current_password: 'a',
new_password: 'b', new_password: 'b',
}).success, }).success,
).toBe(true); ).toBe(true);
expect( expect(changePasswordRequestSchema.safeParse({ new_password: 'b' }).success).toBe(false);
changePasswordRequestSchema.safeParse({ new_password: 'b' }).success,
).toBe(false);
}); });
}); });
describe('mfa + mcp-token schemas', () => { describe('mfa + mcp-token schemas', () => {
it('validate their fields', () => { it('validate their fields', () => {
expect( expect(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' }).success).toBe(true);
mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' }) expect(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't' }).success).toBe(false);
.success, expect(mfaEnableRequestSchema.safeParse({ code: '123456' }).success).toBe(true);
).toBe(true); expect(mcpTokenCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe(true);
expect(
mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't' }).success,
).toBe(false);
expect(mfaEnableRequestSchema.safeParse({ code: '123456' }).success).toBe(
true,
);
expect(mcpTokenCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe(
true,
);
expect(mcpTokenCreateRequestSchema.safeParse({}).success).toBe(true); expect(mcpTokenCreateRequestSchema.safeParse({}).success).toBe(true);
}); });
}); });
+2 -7
View File
@@ -11,16 +11,11 @@ describe('autoBackupSettingsRequestSchema', () => {
keep_days: 7, keep_days: 7,
}).success, }).success,
).toBe(true); ).toBe(true);
expect( expect(autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' }).success).toBe(true);
autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' })
.success,
).toBe(true);
expect(autoBackupSettingsRequestSchema.safeParse({}).success).toBe(true); expect(autoBackupSettingsRequestSchema.safeParse({}).success).toBe(true);
}); });
it('rejects a non-boolean enabled', () => { it('rejects a non-boolean enabled', () => {
expect( expect(autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false);
autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success,
).toBe(false);
}); });
}); });
+1 -3
View File
@@ -16,6 +16,4 @@ export const autoBackupSettingsRequestSchema = z
time: z.string().optional(), time: z.string().optional(),
}) })
.passthrough(); .passthrough();
export type AutoBackupSettingsRequest = z.infer< export type AutoBackupSettingsRequest = z.infer<typeof autoBackupSettingsRequestSchema>;
typeof autoBackupSettingsRequestSchema
>;
+7 -22
View File
@@ -9,9 +9,7 @@ import { describe, it, expect } from 'vitest';
describe('budgetCreateItemRequestSchema', () => { describe('budgetCreateItemRequestSchema', () => {
it('requires a name; money/meta fields optional + nullable', () => { it('requires a name; money/meta fields optional + nullable', () => {
expect( expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success).toBe(true);
budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success,
).toBe(true);
expect( expect(
budgetCreateItemRequestSchema.safeParse({ budgetCreateItemRequestSchema.safeParse({
name: 'Hotel', name: 'Hotel',
@@ -25,34 +23,21 @@ describe('budgetCreateItemRequestSchema', () => {
describe('budgetUpdateMembersRequestSchema', () => { describe('budgetUpdateMembersRequestSchema', () => {
it('requires a numeric user_ids array', () => { it('requires a numeric user_ids array', () => {
expect( expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success, expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
).toBe(true);
expect(
budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success,
).toBe(false);
}); });
}); });
describe('budgetToggleMemberPaidRequestSchema', () => { describe('budgetToggleMemberPaidRequestSchema', () => {
it('requires a boolean paid', () => { it('requires a boolean paid', () => {
expect( expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success).toBe(true);
budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success, expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success).toBe(false);
).toBe(true);
expect(
budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success,
).toBe(false);
}); });
}); });
describe('budgetReorderItemsRequestSchema', () => { describe('budgetReorderItemsRequestSchema', () => {
it('requires numeric ids', () => { it('requires numeric ids', () => {
expect( expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true);
budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] }) expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false);
.success,
).toBe(true);
expect(
budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success,
).toBe(false);
}); });
}); });
+16 -24
View File
@@ -145,9 +145,7 @@ export const budgetCreateItemRequestSchema = z.object({
// "add expense" flow). The server stores it on budget_items.reservation_id. // "add expense" flow). The server stores it on budget_items.reservation_id.
reservation_id: z.number().optional(), reservation_id: z.number().optional(),
}); });
export type BudgetCreateItemRequest = z.infer< export type BudgetCreateItemRequest = z.infer<typeof budgetCreateItemRequestSchema>;
typeof budgetCreateItemRequestSchema
>;
/** Update accepts the same fields plus total_price changes; all optional. */ /** Update accepts the same fields plus total_price changes; all optional. */
export const budgetUpdateItemRequestSchema = z.object({ export const budgetUpdateItemRequestSchema = z.object({
@@ -163,17 +161,13 @@ export const budgetUpdateItemRequestSchema = z.object({
note: z.string().nullable().optional(), note: z.string().nullable().optional(),
expense_date: z.string().nullable().optional(), expense_date: z.string().nullable().optional(),
}); });
export type BudgetUpdateItemRequest = z.infer< export type BudgetUpdateItemRequest = z.infer<typeof budgetUpdateItemRequestSchema>;
typeof budgetUpdateItemRequestSchema
>;
/** Replace the explicit payers of an expense (amounts in expense currency). */ /** Replace the explicit payers of an expense (amounts in expense currency). */
export const budgetUpdatePayersRequestSchema = z.object({ export const budgetUpdatePayersRequestSchema = z.object({
payers: z.array(payerInputSchema), payers: z.array(payerInputSchema),
}); });
export type BudgetUpdatePayersRequest = z.infer< export type BudgetUpdatePayersRequest = z.infer<typeof budgetUpdatePayersRequestSchema>;
typeof budgetUpdatePayersRequestSchema
>;
/** /**
* A persisted settle-up transfer (budget_settlements row): "from paid to" a * A persisted settle-up transfer (budget_settlements row): "from paid to" a
@@ -200,34 +194,32 @@ export const budgetCreateSettlementRequestSchema = z.object({
to_user_id: z.number(), to_user_id: z.number(),
amount: z.number(), amount: z.number(),
}); });
export type BudgetCreateSettlementRequest = z.infer< export type BudgetCreateSettlementRequest = z.infer<typeof budgetCreateSettlementRequestSchema>;
typeof budgetCreateSettlementRequestSchema
>; /** Edit a persisted settle-up transfer (same fields as create; full replace). */
export const budgetUpdateSettlementRequestSchema = z.object({
from_user_id: z.number(),
to_user_id: z.number(),
amount: z.number(),
});
export type BudgetUpdateSettlementRequest = z.infer<typeof budgetUpdateSettlementRequestSchema>;
export const budgetUpdateMembersRequestSchema = z.object({ export const budgetUpdateMembersRequestSchema = z.object({
user_ids: z.array(z.number()), user_ids: z.array(z.number()),
}); });
export type BudgetUpdateMembersRequest = z.infer< export type BudgetUpdateMembersRequest = z.infer<typeof budgetUpdateMembersRequestSchema>;
typeof budgetUpdateMembersRequestSchema
>;
export const budgetToggleMemberPaidRequestSchema = z.object({ export const budgetToggleMemberPaidRequestSchema = z.object({
paid: z.boolean(), paid: z.boolean(),
}); });
export type BudgetToggleMemberPaidRequest = z.infer< export type BudgetToggleMemberPaidRequest = z.infer<typeof budgetToggleMemberPaidRequestSchema>;
typeof budgetToggleMemberPaidRequestSchema
>;
export const budgetReorderItemsRequestSchema = z.object({ export const budgetReorderItemsRequestSchema = z.object({
orderedIds: z.array(z.number()), orderedIds: z.array(z.number()),
}); });
export type BudgetReorderItemsRequest = z.infer< export type BudgetReorderItemsRequest = z.infer<typeof budgetReorderItemsRequestSchema>;
typeof budgetReorderItemsRequestSchema
>;
export const budgetReorderCategoriesRequestSchema = z.object({ export const budgetReorderCategoriesRequestSchema = z.object({
orderedCategories: z.array(z.string()), orderedCategories: z.array(z.string()),
}); });
export type BudgetReorderCategoriesRequest = z.infer< export type BudgetReorderCategoriesRequest = z.infer<typeof budgetReorderCategoriesRequestSchema>;
typeof budgetReorderCategoriesRequestSchema
>;
+4 -14
View File
@@ -1,8 +1,4 @@
import { import { categorySchema, createCategoryRequestSchema, updateCategoryRequestSchema } from './category.schema';
categorySchema,
createCategoryRequestSchema,
updateCategoryRequestSchema,
} from './category.schema';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
@@ -21,12 +17,8 @@ describe('categorySchema', () => {
describe('createCategoryRequestSchema', () => { describe('createCategoryRequestSchema', () => {
it('requires a non-empty name; colour and icon are optional', () => { it('requires a non-empty name; colour and icon are optional', () => {
expect( expect(createCategoryRequestSchema.safeParse({ name: 'Food' }).success).toBe(true);
createCategoryRequestSchema.safeParse({ name: 'Food' }).success, expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(false);
).toBe(true);
expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(
false,
);
expect(createCategoryRequestSchema.safeParse({}).success).toBe(false); expect(createCategoryRequestSchema.safeParse({}).success).toBe(false);
}); });
}); });
@@ -34,8 +26,6 @@ describe('createCategoryRequestSchema', () => {
describe('updateCategoryRequestSchema', () => { describe('updateCategoryRequestSchema', () => {
it('allows every field to be omitted (the service COALESCEs)', () => { it('allows every field to be omitted (the service COALESCEs)', () => {
expect(updateCategoryRequestSchema.safeParse({}).success).toBe(true); expect(updateCategoryRequestSchema.safeParse({}).success).toBe(true);
expect( expect(updateCategoryRequestSchema.safeParse({ color: '#000' }).success).toBe(true);
updateCategoryRequestSchema.safeParse({ color: '#000' }).success,
).toBe(true);
}); });
}); });
+11 -36
View File
@@ -10,12 +10,8 @@ import { describe, it, expect } from 'vitest';
describe('collabNoteCreateRequestSchema', () => { describe('collabNoteCreateRequestSchema', () => {
it('requires a non-empty title; the rest is optional', () => { it('requires a non-empty title; the rest is optional', () => {
expect( expect(collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success).toBe(true);
collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success, expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(false);
).toBe(true);
expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(
false,
);
expect(collabNoteCreateRequestSchema.safeParse({}).success).toBe(false); expect(collabNoteCreateRequestSchema.safeParse({}).success).toBe(false);
}); });
}); });
@@ -34,50 +30,29 @@ describe('collabPollCreateRequestSchema', () => {
options: ['A'], options: ['A'],
}).success, }).success,
).toBe(false); ).toBe(false);
expect( expect(collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success).toBe(false);
collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success,
).toBe(false);
}); });
}); });
describe('collabPollVoteRequestSchema', () => { describe('collabPollVoteRequestSchema', () => {
it('requires a numeric option_index', () => { it('requires a numeric option_index', () => {
expect( expect(collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success).toBe(true);
collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success, expect(collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success).toBe(false);
).toBe(true);
expect(
collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success,
).toBe(false);
}); });
}); });
describe('collabMessageCreateRequestSchema', () => { describe('collabMessageCreateRequestSchema', () => {
it('requires text, caps it at 5000, allows a nullable reply_to', () => { it('requires text, caps it at 5000, allows a nullable reply_to', () => {
expect( expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }).success).toBe(true);
collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }) expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: 4 }).success).toBe(true);
.success, expect(collabMessageCreateRequestSchema.safeParse({ text: '' }).success).toBe(false);
).toBe(true); expect(collabMessageCreateRequestSchema.safeParse({ text: 'x'.repeat(5001) }).success).toBe(false);
expect(
collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: 4 })
.success,
).toBe(true);
expect(
collabMessageCreateRequestSchema.safeParse({ text: '' }).success,
).toBe(false);
expect(
collabMessageCreateRequestSchema.safeParse({ text: 'x'.repeat(5001) })
.success,
).toBe(false);
}); });
}); });
describe('collabReactionRequestSchema', () => { describe('collabReactionRequestSchema', () => {
it('requires a non-empty emoji', () => { it('requires a non-empty emoji', () => {
expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe( expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(true);
true, expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(false);
);
expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(
false,
);
}); });
}); });
+4 -12
View File
@@ -18,9 +18,7 @@ export const collabNoteCreateRequestSchema = z.object({
color: z.string().optional(), color: z.string().optional(),
website: z.string().optional(), website: z.string().optional(),
}); });
export type CollabNoteCreateRequest = z.infer< export type CollabNoteCreateRequest = z.infer<typeof collabNoteCreateRequestSchema>;
typeof collabNoteCreateRequestSchema
>;
export const collabNoteUpdateRequestSchema = z.object({ export const collabNoteUpdateRequestSchema = z.object({
title: z.string().optional(), title: z.string().optional(),
@@ -30,9 +28,7 @@ export const collabNoteUpdateRequestSchema = z.object({
pinned: z.union([z.boolean(), z.number()]).optional(), pinned: z.union([z.boolean(), z.number()]).optional(),
website: z.string().optional(), website: z.string().optional(),
}); });
export type CollabNoteUpdateRequest = z.infer< export type CollabNoteUpdateRequest = z.infer<typeof collabNoteUpdateRequestSchema>;
typeof collabNoteUpdateRequestSchema
>;
export const collabPollCreateRequestSchema = z.object({ export const collabPollCreateRequestSchema = z.object({
question: z.string().min(1), question: z.string().min(1),
@@ -41,9 +37,7 @@ export const collabPollCreateRequestSchema = z.object({
multiple_choice: z.boolean().optional(), multiple_choice: z.boolean().optional(),
deadline: z.string().optional(), deadline: z.string().optional(),
}); });
export type CollabPollCreateRequest = z.infer< export type CollabPollCreateRequest = z.infer<typeof collabPollCreateRequestSchema>;
typeof collabPollCreateRequestSchema
>;
export const collabPollVoteRequestSchema = z.object({ export const collabPollVoteRequestSchema = z.object({
option_index: z.number(), option_index: z.number(),
@@ -54,9 +48,7 @@ export const collabMessageCreateRequestSchema = z.object({
text: z.string().min(1).max(5000), text: z.string().min(1).max(5000),
reply_to: z.number().nullable().optional(), reply_to: z.number().nullable().optional(),
}); });
export type CollabMessageCreateRequest = z.infer< export type CollabMessageCreateRequest = z.infer<typeof collabMessageCreateRequestSchema>;
typeof collabMessageCreateRequestSchema
>;
export const collabReactionRequestSchema = z.object({ export const collabReactionRequestSchema = z.object({
emoji: z.string().min(1), emoji: z.string().min(1),
+2 -9
View File
@@ -1,10 +1,5 @@
import { paginationQuerySchema } from './pagination.schema'; import { paginationQuerySchema } from './pagination.schema';
import { import { idSchema, idParamSchema, nonEmptyString, isoDateTime } from './primitives.schema';
idSchema,
idParamSchema,
nonEmptyString,
isoDateTime,
} from './primitives.schema';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
@@ -43,8 +38,6 @@ describe('@trek/shared pagination', () => {
it('enforces bounds', () => { it('enforces bounds', () => {
expect(paginationQuerySchema.safeParse({ perPage: 0 }).success).toBe(false); expect(paginationQuerySchema.safeParse({ perPage: 0 }).success).toBe(false);
expect(paginationQuerySchema.safeParse({ perPage: 999 }).success).toBe( expect(paginationQuerySchema.safeParse({ perPage: 999 }).success).toBe(false);
false,
);
}); });
}); });
+7 -24
View File
@@ -1,32 +1,19 @@
import { import { dayCreateRequestSchema, dayNoteCreateRequestSchema, dayNoteUpdateRequestSchema } from './day.schema';
dayCreateRequestSchema,
dayNoteCreateRequestSchema,
dayNoteUpdateRequestSchema,
} from './day.schema';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
describe('dayCreateRequestSchema', () => { describe('dayCreateRequestSchema', () => {
it('accepts an optional date + notes', () => { it('accepts an optional date + notes', () => {
expect(dayCreateRequestSchema.safeParse({}).success).toBe(true); expect(dayCreateRequestSchema.safeParse({}).success).toBe(true);
expect( expect(dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' }).success).toBe(true);
dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' })
.success,
).toBe(true);
}); });
}); });
describe('dayNoteCreateRequestSchema', () => { describe('dayNoteCreateRequestSchema', () => {
it('requires non-empty text capped at 500, time capped at 250', () => { it('requires non-empty text capped at 500, time capped at 250', () => {
expect( expect(dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success).toBe(true);
dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success, expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe(false);
).toBe(true); expect(dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false);
expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe(
false,
);
expect(
dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success,
).toBe(false);
expect( expect(
dayNoteCreateRequestSchema.safeParse({ dayNoteCreateRequestSchema.safeParse({
text: 'ok', text: 'ok',
@@ -39,11 +26,7 @@ describe('dayNoteCreateRequestSchema', () => {
describe('dayNoteUpdateRequestSchema', () => { describe('dayNoteUpdateRequestSchema', () => {
it('allows omitting text and caps the lengths', () => { it('allows omitting text and caps the lengths', () => {
expect(dayNoteUpdateRequestSchema.safeParse({}).success).toBe(true); expect(dayNoteUpdateRequestSchema.safeParse({}).success).toBe(true);
expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe( expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(true);
true, expect(dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false);
);
expect(
dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success,
).toBe(false);
}); });
}); });
+5 -20
View File
@@ -1,34 +1,19 @@
import { import { fileUpdateRequestSchema, fileLinkRequestSchema, photoVariantSchema } from './file.schema';
fileUpdateRequestSchema,
fileLinkRequestSchema,
photoVariantSchema,
} from './file.schema';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
describe('fileUpdateRequestSchema', () => { describe('fileUpdateRequestSchema', () => {
it('accepts optional metadata, nullable ids, an empty body', () => { it('accepts optional metadata, nullable ids, an empty body', () => {
expect( expect(fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 }).success).toBe(true);
fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 }) expect(fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' }).success).toBe(true);
.success,
).toBe(true);
expect(
fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' })
.success,
).toBe(true);
expect(fileUpdateRequestSchema.safeParse({}).success).toBe(true); expect(fileUpdateRequestSchema.safeParse({}).success).toBe(true);
}); });
}); });
describe('fileLinkRequestSchema', () => { describe('fileLinkRequestSchema', () => {
it('accepts any subset of reservation/assignment/place ids', () => { it('accepts any subset of reservation/assignment/place ids', () => {
expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe( expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(true);
true, expect(fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null }).success).toBe(true);
);
expect(
fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null })
.success,
).toBe(true);
expect(fileLinkRequestSchema.safeParse({}).success).toBe(true); expect(fileLinkRequestSchema.safeParse({}).success).toBe(true);
}); });
}); });
+37 -68
View File
@@ -2,8 +2,7 @@ import type { TranslationStrings } from '../types';
const admin: TranslationStrings = { const admin: TranslationStrings = {
'admin.notifications.title': 'الإشعارات', 'admin.notifications.title': 'الإشعارات',
'admin.notifications.hint': 'admin.notifications.hint': 'اختر قناة إشعارات واحدة. يمكن تفعيل واحدة فقط في كل مرة.',
'اختر قناة إشعارات واحدة. يمكن تفعيل واحدة فقط في كل مرة.',
'admin.notifications.none': 'معطّل', 'admin.notifications.none': 'معطّل',
'admin.notifications.email': 'البريد الإلكتروني (SMTP)', 'admin.notifications.email': 'البريد الإلكتروني (SMTP)',
'admin.ntfy.hint': 'admin.ntfy.hint':
@@ -16,18 +15,14 @@ const admin: TranslationStrings = {
'admin.notifications.testNtfy': 'إرسال Ntfy تجريبي', 'admin.notifications.testNtfy': 'إرسال Ntfy تجريبي',
'admin.notifications.testNtfySuccess': 'تم إرسال Ntfy التجريبي بنجاح', 'admin.notifications.testNtfySuccess': 'تم إرسال Ntfy التجريبي بنجاح',
'admin.notifications.testNtfyFailed': 'فشل إرسال Ntfy التجريبي', 'admin.notifications.testNtfyFailed': 'فشل إرسال Ntfy التجريبي',
'admin.notifications.inappPanel.hint': 'admin.notifications.inappPanel.hint': 'الإشعارات داخل التطبيق نشطة دائمًا ولا يمكن تعطيلها بشكل عام.',
'الإشعارات داخل التطبيق نشطة دائمًا ولا يمكن تعطيلها بشكل عام.',
'admin.notifications.adminWebhookPanel.title': 'Webhook المسؤول', 'admin.notifications.adminWebhookPanel.title': 'Webhook المسؤول',
'admin.notifications.adminWebhookPanel.hint': 'admin.notifications.adminWebhookPanel.hint':
'يُستخدم هذا الـ Webhook حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن Webhooks المستخدمين ويُرسل تلقائيًا عند تعيين رابط URL.', 'يُستخدم هذا الـ Webhook حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن Webhooks المستخدمين ويُرسل تلقائيًا عند تعيين رابط URL.',
'admin.notifications.adminWebhookPanel.saved': 'تم حفظ رابط Webhook المسؤول', 'admin.notifications.adminWebhookPanel.saved': 'تم حفظ رابط Webhook المسؤول',
'admin.notifications.adminWebhookPanel.testSuccess': 'admin.notifications.adminWebhookPanel.testSuccess': 'تم إرسال Webhook الاختباري بنجاح',
'تم إرسال Webhook الاختباري بنجاح', 'admin.notifications.adminWebhookPanel.testFailed': 'فشل إرسال Webhook الاختباري',
'admin.notifications.adminWebhookPanel.testFailed': 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'يُرسل Webhook المسؤول تلقائيًا عند تعيين رابط URL',
'فشل إرسال Webhook الاختباري',
'admin.notifications.adminWebhookPanel.alwaysOnHint':
'يُرسل Webhook المسؤول تلقائيًا عند تعيين رابط URL',
'admin.notifications.adminNtfyPanel.title': 'Ntfy المسؤول', 'admin.notifications.adminNtfyPanel.title': 'Ntfy المسؤول',
'admin.notifications.adminNtfyPanel.hint': 'admin.notifications.adminNtfyPanel.hint':
'يُستخدم موضوع Ntfy هذا حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن مواضيع المستخدمين ويُرسل دائمًا عند تهيئته.', 'يُستخدم موضوع Ntfy هذا حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن مواضيع المستخدمين ويُرسل دائمًا عند تهيئته.',
@@ -39,23 +34,19 @@ const admin: TranslationStrings = {
'admin.notifications.adminNtfyPanel.tokenCleared': 'تم مسح رمز وصول المسؤول', 'admin.notifications.adminNtfyPanel.tokenCleared': 'تم مسح رمز وصول المسؤول',
'admin.notifications.adminNtfyPanel.saved': 'تم حفظ إعدادات Ntfy للمسؤول', 'admin.notifications.adminNtfyPanel.saved': 'تم حفظ إعدادات Ntfy للمسؤول',
'admin.notifications.adminNtfyPanel.test': 'إرسال Ntfy تجريبي', 'admin.notifications.adminNtfyPanel.test': 'إرسال Ntfy تجريبي',
'admin.notifications.adminNtfyPanel.testSuccess': 'admin.notifications.adminNtfyPanel.testSuccess': 'تم إرسال Ntfy التجريبي بنجاح',
'تم إرسال Ntfy التجريبي بنجاح',
'admin.notifications.adminNtfyPanel.testFailed': 'فشل إرسال Ntfy التجريبي', 'admin.notifications.adminNtfyPanel.testFailed': 'فشل إرسال Ntfy التجريبي',
'admin.notifications.adminNtfyPanel.alwaysOnHint': 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'يُرسل Ntfy للمسؤول دائمًا عند تهيئة موضوع',
'يُرسل Ntfy للمسؤول دائمًا عند تهيئة موضوع',
'admin.notifications.adminNotificationsHint': 'admin.notifications.adminNotificationsHint':
'حدد القنوات التي تُسلّم إشعارات المسؤول (مثل تنبيهات الإصدارات). يُرسل الـ Webhook تلقائيًا عند تعيين رابط URL لـ Webhook المسؤول.', 'حدد القنوات التي تُسلّم إشعارات المسؤول (مثل تنبيهات الإصدارات). يُرسل الـ Webhook تلقائيًا عند تعيين رابط URL لـ Webhook المسؤول.',
'admin.notifications.tripReminders.title': 'تذكيرات الرحلات', 'admin.notifications.tripReminders.title': 'تذكيرات الرحلات',
'admin.notifications.tripReminders.hint': 'admin.notifications.tripReminders.hint': 'إرسال تذكير قبل بدء الرحلة (يتطلب تعيين أيام التذكير على الرحلة).',
'إرسال تذكير قبل بدء الرحلة (يتطلب تعيين أيام التذكير على الرحلة).',
'admin.notifications.tripReminders.enabled': 'تم تفعيل تذكيرات الرحلات', 'admin.notifications.tripReminders.enabled': 'تم تفعيل تذكيرات الرحلات',
'admin.notifications.tripReminders.disabled': 'تم تعطيل تذكيرات الرحلات', 'admin.notifications.tripReminders.disabled': 'تم تعطيل تذكيرات الرحلات',
'admin.smtp.title': 'البريد والإشعارات', 'admin.smtp.title': 'البريد والإشعارات',
'admin.smtp.hint': 'تكوين SMTP لإرسال إشعارات البريد الإلكتروني.', 'admin.smtp.hint': 'تكوين SMTP لإرسال إشعارات البريد الإلكتروني.',
'admin.smtp.testButton': 'إرسال بريد تجريبي', 'admin.smtp.testButton': 'إرسال بريد تجريبي',
'admin.webhook.hint': 'admin.webhook.hint': 'إرسال الإشعارات إلى webhook خارجي (Discord، Slack، إلخ).',
'إرسال الإشعارات إلى webhook خارجي (Discord، Slack، إلخ).',
'admin.smtp.testSuccess': 'تم إرسال البريد التجريبي بنجاح', 'admin.smtp.testSuccess': 'تم إرسال البريد التجريبي بنجاح',
'admin.smtp.testFailed': 'فشل إرسال البريد التجريبي', 'admin.smtp.testFailed': 'فشل إرسال البريد التجريبي',
'admin.title': 'الإدارة', 'admin.title': 'الإدارة',
@@ -91,8 +82,7 @@ const admin: TranslationStrings = {
'admin.toast.cannotDeleteSelf': 'لا يمكنك حذف حسابك الخاص', 'admin.toast.cannotDeleteSelf': 'لا يمكنك حذف حسابك الخاص',
'admin.toast.userCreated': 'تم إنشاء المستخدم', 'admin.toast.userCreated': 'تم إنشاء المستخدم',
'admin.toast.createError': 'فشل إنشاء المستخدم', 'admin.toast.createError': 'فشل إنشاء المستخدم',
'admin.toast.fieldsRequired': 'admin.toast.fieldsRequired': 'اسم المستخدم والبريد الإلكتروني وكلمة المرور مطلوبة',
'اسم المستخدم والبريد الإلكتروني وكلمة المرور مطلوبة',
'admin.createUser': 'إنشاء مستخدم', 'admin.createUser': 'إنشاء مستخدم',
'admin.invite.title': 'روابط الدعوة', 'admin.invite.title': 'روابط الدعوة',
'admin.invite.subtitle': 'إنشاء روابط تسجيل للاستخدام المحدود', 'admin.invite.subtitle': 'إنشاء روابط تسجيل للاستخدام المحدود',
@@ -116,14 +106,11 @@ const admin: TranslationStrings = {
'admin.allowRegistration': 'السماح بالتسجيل', 'admin.allowRegistration': 'السماح بالتسجيل',
'admin.allowRegistrationHint': 'يمكن للمستخدمين الجدد التسجيل بأنفسهم', 'admin.allowRegistrationHint': 'يمكن للمستخدمين الجدد التسجيل بأنفسهم',
'admin.requireMfa': 'فرض المصادقة الثنائية (2FA)', 'admin.requireMfa': 'فرض المصادقة الثنائية (2FA)',
'admin.requireMfaHint': 'admin.requireMfaHint': 'يجب على المستخدمين الذين لا يملكون 2FA إكمال الإعداد في الإعدادات قبل استخدام التطبيق.',
'يجب على المستخدمين الذين لا يملكون 2FA إكمال الإعداد في الإعدادات قبل استخدام التطبيق.',
'admin.apiKeys': 'مفاتيح API', 'admin.apiKeys': 'مفاتيح API',
'admin.apiKeysHint': 'admin.apiKeysHint': 'اختياري. يُفعّل بيانات الأماكن الموسعة مثل الصور والطقس.',
'اختياري. يُفعّل بيانات الأماكن الموسعة مثل الصور والطقس.',
'admin.mapsKey': 'مفتاح Google Maps API', 'admin.mapsKey': 'مفتاح Google Maps API',
'admin.mapsKeyHint': 'admin.mapsKeyHint': 'مطلوب للبحث عن الأماكن. احصل عليه من console.cloud.google.com',
'مطلوب للبحث عن الأماكن. احصل عليه من console.cloud.google.com',
'admin.mapsKeyHintLong': 'admin.mapsKeyHintLong':
'بدون مفتاح API، يُستخدم OpenStreetMap للبحث. مع مفتاح Google يمكن تحميل الصور والتقييمات وساعات العمل أيضًا. احصل عليه من console.cloud.google.com.', 'بدون مفتاح API، يُستخدم OpenStreetMap للبحث. مع مفتاح Google يمكن تحميل الصور والتقييمات وساعات العمل أيضًا. احصل عليه من console.cloud.google.com.',
'admin.recommended': 'مُوصى به', 'admin.recommended': 'مُوصى به',
@@ -134,27 +121,22 @@ const admin: TranslationStrings = {
'admin.keyInvalid': 'غير صالح', 'admin.keyInvalid': 'غير صالح',
'admin.keySaved': 'تم حفظ مفاتيح API', 'admin.keySaved': 'تم حفظ مفاتيح API',
'admin.oidcTitle': 'تسجيل الدخول الموحد (OIDC)', 'admin.oidcTitle': 'تسجيل الدخول الموحد (OIDC)',
'admin.oidcSubtitle': 'admin.oidcSubtitle': 'السماح بتسجيل الدخول عبر مزودين خارجيين مثل Google أو Apple أو Authentik أو Keycloak.',
'السماح بتسجيل الدخول عبر مزودين خارجيين مثل Google أو Apple أو Authentik أو Keycloak.',
'admin.oidcDisplayName': 'الاسم المعروض', 'admin.oidcDisplayName': 'الاسم المعروض',
'admin.oidcIssuer': 'عنوان URL للمُصدر', 'admin.oidcIssuer': 'عنوان URL للمُصدر',
'admin.oidcIssuerHint': 'admin.oidcIssuerHint': 'عنوان OpenID Connect Issuer URL للمزود. مثال: https://accounts.google.com',
'عنوان OpenID Connect Issuer URL للمزود. مثال: https://accounts.google.com',
'admin.oidcSaved': 'تم حفظ إعدادات OIDC', 'admin.oidcSaved': 'تم حفظ إعدادات OIDC',
'admin.oidcOnlyMode': 'تعطيل المصادقة بكلمة المرور', 'admin.oidcOnlyMode': 'تعطيل المصادقة بكلمة المرور',
'admin.oidcOnlyModeHint': 'admin.oidcOnlyModeHint':
'عند التفعيل، يُسمح فقط بتسجيل الدخول عبر SSO. سيتم حظر تسجيل الدخول والتسجيل بكلمة المرور.', 'عند التفعيل، يُسمح فقط بتسجيل الدخول عبر SSO. سيتم حظر تسجيل الدخول والتسجيل بكلمة المرور.',
'admin.fileTypes': 'أنواع الملفات المسموح بها', 'admin.fileTypes': 'أنواع الملفات المسموح بها',
'admin.fileTypesHint': 'حدد أنواع الملفات التي يمكن للمستخدمين رفعها.', 'admin.fileTypesHint': 'حدد أنواع الملفات التي يمكن للمستخدمين رفعها.',
'admin.fileTypesFormat': 'admin.fileTypesFormat': 'امتدادات مفصولة بفواصل (مثل jpg,png,pdf,doc). استخدم * للسماح بجميع الأنواع.',
'امتدادات مفصولة بفواصل (مثل jpg,png,pdf,doc). استخدم * للسماح بجميع الأنواع.',
'admin.fileTypesSaved': 'تم حفظ إعدادات أنواع الملفات', 'admin.fileTypesSaved': 'تم حفظ إعدادات أنواع الملفات',
'admin.placesPhotos.title': 'صور الأماكن', 'admin.placesPhotos.title': 'صور الأماكن',
'admin.placesPhotos.subtitle': 'admin.placesPhotos.subtitle': 'جلب الصور من Google Places API. عطّلها للحفاظ على حصة API. صور Wikimedia غير متأثرة.',
'جلب الصور من Google Places API. عطّلها للحفاظ على حصة API. صور Wikimedia غير متأثرة.',
'admin.placesAutocomplete.title': 'الإكمال التلقائي للأماكن', 'admin.placesAutocomplete.title': 'الإكمال التلقائي للأماكن',
'admin.placesAutocomplete.subtitle': 'admin.placesAutocomplete.subtitle': 'استخدام Google Places API لاقتراحات البحث. عطّلها للحفاظ على حصة API.',
'استخدام Google Places API لاقتراحات البحث. عطّلها للحفاظ على حصة API.',
'admin.placesDetails.title': 'تفاصيل الأماكن', 'admin.placesDetails.title': 'تفاصيل الأماكن',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'جلب معلومات تفصيلية عن الأماكن (الساعات، التقييم، الموقع) من Google Places API. عطّلها للحفاظ على حصة API.', 'جلب معلومات تفصيلية عن الأماكن (الساعات، التقييم، الموقع) من Google Places API. عطّلها للحفاظ على حصة API.',
@@ -206,15 +188,12 @@ const admin: TranslationStrings = {
'admin.addons.catalog.vacay.name': 'الإجازة', 'admin.addons.catalog.vacay.name': 'الإجازة',
'admin.addons.catalog.vacay.description': 'مخطط إجازات شخصي مع عرض تقويم', 'admin.addons.catalog.vacay.description': 'مخطط إجازات شخصي مع عرض تقويم',
'admin.addons.catalog.atlas.name': 'الأطلس', 'admin.addons.catalog.atlas.name': 'الأطلس',
'admin.addons.catalog.atlas.description': 'admin.addons.catalog.atlas.description': 'خريطة العالم مع الدول التي تمت زيارتها وإحصائيات السفر',
'خريطة العالم مع الدول التي تمت زيارتها وإحصائيات السفر',
'admin.addons.catalog.collab.name': 'التعاون', 'admin.addons.catalog.collab.name': 'التعاون',
'admin.addons.catalog.collab.description': 'admin.addons.catalog.collab.description': 'ملاحظات واستطلاعات ودردشة لحظية لتخطيط الرحلة',
'ملاحظات واستطلاعات ودردشة لحظية لتخطيط الرحلة',
'admin.addons.catalog.memories.name': 'صور (Immich)', 'admin.addons.catalog.memories.name': 'صور (Immich)',
'admin.addons.catalog.memories.description': 'شارك صور رحلتك عبر Immich', 'admin.addons.catalog.memories.description': 'شارك صور رحلتك عبر Immich',
'admin.addons.catalog.mcp.description': 'admin.addons.catalog.mcp.description': 'بروتوكول سياق النموذج لتكامل مساعد الذكاء الاصطناعي',
'بروتوكول سياق النموذج لتكامل مساعد الذكاء الاصطناعي',
'admin.addons.subtitleBefore': 'فعّل أو عطّل الميزات لتخصيص تجربة ', 'admin.addons.subtitleBefore': 'فعّل أو عطّل الميزات لتخصيص تجربة ',
'admin.addons.subtitleAfter': '.', 'admin.addons.subtitleAfter': '.',
'admin.addons.enabled': 'مفعّل', 'admin.addons.enabled': 'مفعّل',
@@ -224,8 +203,7 @@ const admin: TranslationStrings = {
'admin.addons.type.integration': 'تكامل', 'admin.addons.type.integration': 'تكامل',
'admin.addons.tripHint': 'متاح كعلامة تبويب داخل كل رحلة', 'admin.addons.tripHint': 'متاح كعلامة تبويب داخل كل رحلة',
'admin.addons.globalHint': 'متاح كقسم مستقل في التنقل الرئيسي', 'admin.addons.globalHint': 'متاح كقسم مستقل في التنقل الرئيسي',
'admin.addons.integrationHint': 'admin.addons.integrationHint': 'خدمات الواجهة الخلفية وتكاملات API بدون صفحة مخصصة',
'خدمات الواجهة الخلفية وتكاملات API بدون صفحة مخصصة',
'admin.addons.toast.updated': 'تم تحديث الإضافة', 'admin.addons.toast.updated': 'تم تحديث الإضافة',
'admin.addons.toast.error': 'فشل تحديث الإضافة', 'admin.addons.toast.error': 'فشل تحديث الإضافة',
'admin.addons.noAddons': 'لا توجد إضافات متاحة', 'admin.addons.noAddons': 'لا توجد إضافات متاحة',
@@ -236,8 +214,7 @@ const admin: TranslationStrings = {
'admin.weather.forecast': 'توقعات 16 يومًا', 'admin.weather.forecast': 'توقعات 16 يومًا',
'admin.weather.forecastDesc': 'سابقًا 5 أيام (OpenWeatherMap)', 'admin.weather.forecastDesc': 'سابقًا 5 أيام (OpenWeatherMap)',
'admin.weather.climate': 'بيانات المناخ التاريخية', 'admin.weather.climate': 'بيانات المناخ التاريخية',
'admin.weather.climateDesc': 'admin.weather.climateDesc': 'متوسطات آخر 85 سنة للأيام بعد توقعات الـ 16 يومًا',
'متوسطات آخر 85 سنة للأيام بعد توقعات الـ 16 يومًا',
'admin.weather.requests': '10,000 طلب / يوم', 'admin.weather.requests': '10,000 طلب / يوم',
'admin.weather.requestsDesc': 'مجاني، بدون مفتاح API', 'admin.weather.requestsDesc': 'مجاني، بدون مفتاح API',
'admin.weather.locationHint': 'admin.weather.locationHint':
@@ -253,8 +230,7 @@ const admin: TranslationStrings = {
'admin.mcpTokens.never': 'أبداً', 'admin.mcpTokens.never': 'أبداً',
'admin.mcpTokens.empty': 'لم يتم إنشاء أي رموز MCP بعد', 'admin.mcpTokens.empty': 'لم يتم إنشاء أي رموز MCP بعد',
'admin.mcpTokens.deleteTitle': 'حذف الرمز', 'admin.mcpTokens.deleteTitle': 'حذف الرمز',
'admin.mcpTokens.deleteMessage': 'admin.mcpTokens.deleteMessage': 'سيتم إلغاء هذا الرمز فوراً. سيفقد المستخدم وصوله إلى MCP عبر هذا الرمز.',
'سيتم إلغاء هذا الرمز فوراً. سيفقد المستخدم وصوله إلى MCP عبر هذا الرمز.',
'admin.mcpTokens.deleteSuccess': 'تم حذف الرمز', 'admin.mcpTokens.deleteSuccess': 'تم حذف الرمز',
'admin.mcpTokens.deleteError': 'فشل حذف الرمز', 'admin.mcpTokens.deleteError': 'فشل حذف الرمز',
'admin.mcpTokens.loadError': 'فشل تحميل الرموز', 'admin.mcpTokens.loadError': 'فشل تحميل الرموز',
@@ -265,13 +241,11 @@ const admin: TranslationStrings = {
'admin.oauthSessions.created': 'تاريخ الإنشاء', 'admin.oauthSessions.created': 'تاريخ الإنشاء',
'admin.oauthSessions.empty': 'لا توجد جلسات OAuth نشطة', 'admin.oauthSessions.empty': 'لا توجد جلسات OAuth نشطة',
'admin.oauthSessions.revokeTitle': 'إلغاء الجلسة', 'admin.oauthSessions.revokeTitle': 'إلغاء الجلسة',
'admin.oauthSessions.revokeMessage': 'admin.oauthSessions.revokeMessage': 'سيتم إلغاء جلسة OAuth هذه فوراً. سيفقد العميل وصوله إلى MCP.',
'سيتم إلغاء جلسة OAuth هذه فوراً. سيفقد العميل وصوله إلى MCP.',
'admin.oauthSessions.revokeSuccess': 'تم إلغاء الجلسة', 'admin.oauthSessions.revokeSuccess': 'تم إلغاء الجلسة',
'admin.oauthSessions.revokeError': 'فشل إلغاء الجلسة', 'admin.oauthSessions.revokeError': 'فشل إلغاء الجلسة',
'admin.oauthSessions.loadError': 'فشل تحميل جلسات OAuth', 'admin.oauthSessions.loadError': 'فشل تحميل جلسات OAuth',
'admin.audit.subtitle': 'admin.audit.subtitle': 'أحداث الأمان والإدارة (النسخ الاحتياطية، المستخدمون، المصادقة الثنائية، الإعدادات).',
'أحداث الأمان والإدارة (النسخ الاحتياطية، المستخدمون، المصادقة الثنائية، الإعدادات).',
'admin.audit.empty': 'لا توجد سجلات تدقيق بعد.', 'admin.audit.empty': 'لا توجد سجلات تدقيق بعد.',
'admin.audit.refresh': 'تحديث', 'admin.audit.refresh': 'تحديث',
'admin.audit.loadMore': 'تحميل المزيد', 'admin.audit.loadMore': 'تحميل المزيد',
@@ -298,12 +272,10 @@ const admin: TranslationStrings = {
'admin.update.button': 'عرض على GitHub', 'admin.update.button': 'عرض على GitHub',
'admin.update.install': 'تثبيت التحديث', 'admin.update.install': 'تثبيت التحديث',
'admin.update.confirmTitle': 'تثبيت التحديث؟', 'admin.update.confirmTitle': 'تثبيت التحديث؟',
'admin.update.confirmText': 'admin.update.confirmText': 'سيتم تحديث TREK من {current} إلى {version}. سيُعاد تشغيل الخادم تلقائيًا بعد ذلك.',
'سيتم تحديث TREK من {current} إلى {version}. سيُعاد تشغيل الخادم تلقائيًا بعد ذلك.',
'admin.update.dataInfo': 'admin.update.dataInfo':
'جميع بياناتك (الرحلات، المستخدمون، مفاتيح API، المرفوعات، الإجازة، الأطلس، الميزانيات) ستبقى محفوظة.', 'جميع بياناتك (الرحلات، المستخدمون، مفاتيح API، المرفوعات، الإجازة، الأطلس، الميزانيات) ستبقى محفوظة.',
'admin.update.warning': 'admin.update.warning': 'سيكون التطبيق غير متاح لفترة وجيزة أثناء إعادة التشغيل.',
'سيكون التطبيق غير متاح لفترة وجيزة أثناء إعادة التشغيل.',
'admin.update.confirm': 'حدّث الآن', 'admin.update.confirm': 'حدّث الآن',
'admin.update.installing': 'جارٍ التحديث…', 'admin.update.installing': 'جارٍ التحديث…',
'admin.update.success': 'تم تثبيت التحديث. ستتم إعادة تشغيل الخادم…', 'admin.update.success': 'تم تثبيت التحديث. ستتم إعادة تشغيل الخادم…',
@@ -311,8 +283,7 @@ const admin: TranslationStrings = {
'admin.update.backupHint': 'نوصي بإنشاء نسخة احتياطية قبل التحديث.', 'admin.update.backupHint': 'نوصي بإنشاء نسخة احتياطية قبل التحديث.',
'admin.update.backupLink': 'الذهاب إلى النسخ الاحتياطي', 'admin.update.backupLink': 'الذهاب إلى النسخ الاحتياطي',
'admin.update.howTo': 'كيفية التحديث', 'admin.update.howTo': 'كيفية التحديث',
'admin.update.dockerText': 'admin.update.dockerText': 'يعمل TREK الخاص بك في Docker. للتحديث إلى {version}، نفّذ الأوامر التالية على الخادم:',
'يعمل TREK الخاص بك في Docker. للتحديث إلى {version}، نفّذ الأوامر التالية على الخادم:',
'admin.update.reloadHint': 'يرجى إعادة تحميل الصفحة بعد بضع ثوانٍ.', 'admin.update.reloadHint': 'يرجى إعادة تحميل الصفحة بعد بضع ثوانٍ.',
'admin.tabs.permissions': 'الصلاحيات', 'admin.tabs.permissions': 'الصلاحيات',
'admin.notifications.webhook': 'Webhook', // en-fallback 'admin.notifications.webhook': 'Webhook', // en-fallback
@@ -326,13 +297,11 @@ const admin: TranslationStrings = {
'admin.passwordLogin': 'Password Login', // en-fallback 'admin.passwordLogin': 'Password Login', // en-fallback
'admin.passwordLoginHint': 'Allow users to sign in with email and password', // en-fallback 'admin.passwordLoginHint': 'Allow users to sign in with email and password', // en-fallback
'admin.passwordRegistration': 'Password Registration', // en-fallback 'admin.passwordRegistration': 'Password Registration', // en-fallback
'admin.passwordRegistrationHint': 'admin.passwordRegistrationHint': 'Allow new users to register with email and password', // en-fallback
'Allow new users to register with email and password', // en-fallback
'admin.oidcLogin': 'SSO Login', // en-fallback 'admin.oidcLogin': 'SSO Login', // en-fallback
'admin.oidcLoginHint': 'Allow users to sign in with SSO', // en-fallback 'admin.oidcLoginHint': 'Allow users to sign in with SSO', // en-fallback
'admin.oidcRegistration': 'SSO Auto-Provisioning', // en-fallback 'admin.oidcRegistration': 'SSO Auto-Provisioning', // en-fallback
'admin.oidcRegistrationHint': 'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users', // en-fallback
'Automatically create accounts for new SSO users', // en-fallback
'admin.envOverrideHint': 'admin.envOverrideHint':
'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.', // en-fallback 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.', // en-fallback
'admin.lockoutWarning': 'At least one login method must remain enabled', // en-fallback 'admin.lockoutWarning': 'At least one login method must remain enabled', // en-fallback
@@ -342,8 +311,7 @@ const admin: TranslationStrings = {
'admin.addons.catalog.journey.description': 'admin.addons.catalog.journey.description':
'Trip tracking & travel journal with check-ins, photos, and daily stories', // en-fallback 'Trip tracking & travel journal with check-ins, photos, and daily stories', // en-fallback
'admin.passkey.title': 'تسجيل الدخول بمفتاح المرور', 'admin.passkey.title': 'تسجيل الدخول بمفتاح المرور',
'admin.passkey.cardHint': 'admin.passkey.cardHint': 'اسمح للمستخدمين بتسجيل الدخول باستخدام مفاتيح المرور (WebAuthn). معطّل افتراضيًا.',
'اسمح للمستخدمين بتسجيل الدخول باستخدام مفاتيح المرور (WebAuthn). معطّل افتراضيًا.',
'admin.passkey.login': 'تفعيل تسجيل الدخول بمفتاح المرور', 'admin.passkey.login': 'تفعيل تسجيل الدخول بمفتاح المرور',
'admin.passkey.loginHint': 'admin.passkey.loginHint':
'إظهار خيار "تسجيل الدخول باستخدام مفتاح المرور" والسماح للمستخدمين بتسجيل مفاتيح المرور في إعداداتهم.', 'إظهار خيار "تسجيل الدخول باستخدام مفتاح المرور" والسماح للمستخدمين بتسجيل مفاتيح المرور في إعداداتهم.',
@@ -353,19 +321,20 @@ const admin: TranslationStrings = {
'admin.passkey.rpIdHint': 'admin.passkey.rpIdHint':
'النطاق المجرّد الذي تُربط به مفاتيح المرور، مثل trek.example.org. اتركه فارغًا لاشتقاقه من APP_URL. تغييره لاحقًا يُبطل مفاتيح المرور الموجودة.', 'النطاق المجرّد الذي تُربط به مفاتيح المرور، مثل trek.example.org. اتركه فارغًا لاشتقاقه من APP_URL. تغييره لاحقًا يُبطل مفاتيح المرور الموجودة.',
'admin.passkey.origins': 'الأصول المسموح بها', 'admin.passkey.origins': 'الأصول المسموح بها',
'admin.passkey.originsHint': 'admin.passkey.originsHint': 'أصول كاملة مفصولة بفواصل، مثل https://trek.example.org. اتركه فارغًا لاستخدام APP_URL.',
'أصول كاملة مفصولة بفواصل، مثل https://trek.example.org. اتركه فارغًا لاستخدام APP_URL.',
'admin.passkey.reset': 'إعادة تعيين مفاتيح المرور', 'admin.passkey.reset': 'إعادة تعيين مفاتيح المرور',
'admin.passkey.resetHint': 'admin.passkey.resetHint':
'إزالة جميع مفاتيح المرور لهذا المستخدم (مثلًا عند فقدان جهاز). سيظل بإمكانه تسجيل الدخول بكلمة المرور.', 'إزالة جميع مفاتيح المرور لهذا المستخدم (مثلًا عند فقدان جهاز). سيظل بإمكانه تسجيل الدخول بكلمة المرور.',
'admin.passkey.resetConfirm': 'إزالة جميع مفاتيح المرور لـ {name}؟', 'admin.passkey.resetConfirm': 'إزالة جميع مفاتيح المرور لـ {name}؟',
'admin.passkey.resetDone': 'تمت إزالة {count} من مفاتيح المرور', 'admin.passkey.resetDone': 'تمت إزالة {count} من مفاتيح المرور',
'admin.defaultSettings.mapProvider': 'محرك الخرائط', 'admin.defaultSettings.mapProvider': 'محرك الخرائط',
'admin.defaultSettings.mapProviderHint': 'الخريطة الافتراضية لجميع المستخدمين على هذا الخادم. لا يزال بإمكان كل مستخدم تجاوزها في إعداداته الخاصة.', 'admin.defaultSettings.mapProviderHint':
'الخريطة الافتراضية لجميع المستخدمين على هذا الخادم. لا يزال بإمكان كل مستخدم تجاوزها في إعداداته الخاصة.',
'admin.defaultSettings.providerLeaflet': 'قياسي (مجاني)', 'admin.defaultSettings.providerLeaflet': 'قياسي (مجاني)',
'admin.defaultSettings.providerMapbox': 'Mapbox (ثلاثي الأبعاد)', 'admin.defaultSettings.providerMapbox': 'Mapbox (ثلاثي الأبعاد)',
'admin.defaultSettings.mapboxToken': 'رمز Mapbox المشترك', 'admin.defaultSettings.mapboxToken': 'رمز Mapbox المشترك',
'admin.defaultSettings.mapboxTokenHint': 'يُستخدم لكل مستخدم لم يُدخل رمزه الخاص — حتى يحصل الخادم بأكمله على Mapbox دون مشاركة المفتاح بشكل فردي. يُخزَّن مشفّرًا.', 'admin.defaultSettings.mapboxTokenHint':
'يُستخدم لكل مستخدم لم يُدخل رمزه الخاص — حتى يحصل الخادم بأكمله على Mapbox دون مشاركة المفتاح بشكل فردي. يُخزَّن مشفّرًا.',
'admin.defaultSettings.mapboxStyle': 'نمط الخريطة', 'admin.defaultSettings.mapboxStyle': 'نمط الخريطة',
'admin.defaultSettings.mapboxStylePlaceholder': 'اختر نمطًا…', 'admin.defaultSettings.mapboxStylePlaceholder': 'اختر نمطًا…',
'admin.defaultSettings.mapbox3d': 'المباني والتضاريس ثلاثية الأبعاد', 'admin.defaultSettings.mapbox3d': 'المباني والتضاريس ثلاثية الأبعاد',
+4 -8
View File
@@ -12,10 +12,8 @@ const backup: TranslationStrings = {
'backup.createFirst': 'إنشاء أول نسخة', 'backup.createFirst': 'إنشاء أول نسخة',
'backup.download': 'تنزيل', 'backup.download': 'تنزيل',
'backup.restore': 'استعادة', 'backup.restore': 'استعادة',
'backup.confirm.restore': 'backup.confirm.restore': 'استعادة النسخة "{name}"؟\n\nسيتم استبدال جميع البيانات الحالية بالنسخة.',
'استعادة النسخة "{name}"؟\n\nسيتم استبدال جميع البيانات الحالية بالنسخة.', 'backup.confirm.uploadRestore': 'رفع واستعادة النسخة "{name}"؟\n\nسيتم الكتابة فوق جميع البيانات الحالية.',
'backup.confirm.uploadRestore':
'رفع واستعادة النسخة "{name}"؟\n\nسيتم الكتابة فوق جميع البيانات الحالية.',
'backup.confirm.delete': 'حذف النسخة "{name}"؟', 'backup.confirm.delete': 'حذف النسخة "{name}"؟',
'backup.toast.loadError': 'فشل تحميل النسخ الاحتياطية', 'backup.toast.loadError': 'فشل تحميل النسخ الاحتياطية',
'backup.toast.created': 'تم إنشاء النسخة الاحتياطية بنجاح', 'backup.toast.created': 'تم إنشاء النسخة الاحتياطية بنجاح',
@@ -31,8 +29,7 @@ const backup: TranslationStrings = {
'backup.auto.title': 'النسخ الاحتياطي التلقائي', 'backup.auto.title': 'النسخ الاحتياطي التلقائي',
'backup.auto.subtitle': 'نسخ احتياطي تلقائي وفق جدول زمني', 'backup.auto.subtitle': 'نسخ احتياطي تلقائي وفق جدول زمني',
'backup.auto.enable': 'تفعيل النسخ التلقائي', 'backup.auto.enable': 'تفعيل النسخ التلقائي',
'backup.auto.enableHint': 'backup.auto.enableHint': 'سيتم إنشاء نسخ احتياطية تلقائيًا وفق الجدول المختار',
'سيتم إنشاء نسخ احتياطية تلقائيًا وفق الجدول المختار',
'backup.auto.interval': 'الفترة', 'backup.auto.interval': 'الفترة',
'backup.auto.hour': 'التنفيذ في الساعة', 'backup.auto.hour': 'التنفيذ في الساعة',
'backup.auto.hourHint': 'التوقيت المحلي للخادم (تنسيق {format})', 'backup.auto.hourHint': 'التوقيت المحلي للخادم (تنسيق {format})',
@@ -68,8 +65,7 @@ const backup: TranslationStrings = {
'backup.restoreConfirmTitle': 'استعادة النسخة الاحتياطية؟', 'backup.restoreConfirmTitle': 'استعادة النسخة الاحتياطية؟',
'backup.restoreWarning': 'backup.restoreWarning':
'سيتم استبدال جميع البيانات الحالية (الرحلات، الأماكن، المستخدمون، المرفوعات) بالنسخة نهائيًا. لا يمكن التراجع عن ذلك.', 'سيتم استبدال جميع البيانات الحالية (الرحلات، الأماكن، المستخدمون، المرفوعات) بالنسخة نهائيًا. لا يمكن التراجع عن ذلك.',
'backup.restoreTip': 'backup.restoreTip': 'نصيحة: أنشئ نسخة احتياطية للحالة الحالية قبل الاستعادة.',
'نصيحة: أنشئ نسخة احتياطية للحالة الحالية قبل الاستعادة.',
'backup.restoreConfirm': 'نعم، استعادة', 'backup.restoreConfirm': 'نعم، استعادة',
'backup.auto.envLocked': 'Docker', // en-fallback 'backup.auto.envLocked': 'Docker', // en-fallback
}; };
+80 -74
View File
@@ -26,8 +26,7 @@ const budget: TranslationStrings = {
'budget.byCategory': 'حسب الفئة', 'budget.byCategory': 'حسب الفئة',
'budget.editTooltip': 'انقر للتعديل', 'budget.editTooltip': 'انقر للتعديل',
'budget.linkedToReservation': 'مرتبط بحجز — عدّل الاسم هناك', 'budget.linkedToReservation': 'مرتبط بحجز — عدّل الاسم هناك',
'budget.confirm.deleteCategory': 'budget.confirm.deleteCategory': 'هل تريد حذف الفئة "{name}" مع {count} إدخالات؟',
'هل تريد حذف الفئة "{name}" مع {count} إدخالات؟',
'budget.deleteCategory': 'حذف الفئة', 'budget.deleteCategory': 'حذف الفئة',
'budget.perPerson': 'لكل شخص', 'budget.perPerson': 'لكل شخص',
'budget.paid': 'مدفوع', 'budget.paid': 'مدفوع',
@@ -38,78 +37,85 @@ const budget: TranslationStrings = {
'انقر على صورة العضو في بند الميزانية لتحديده باللون الأخضر — وهذا يعني أنه دفع. ثم تُظهر التسوية من يدين لمن وبكم.', 'انقر على صورة العضو في بند الميزانية لتحديده باللون الأخضر — وهذا يعني أنه دفع. ثم تُظهر التسوية من يدين لمن وبكم.',
'budget.netBalances': 'الأرصدة الصافية', 'budget.netBalances': 'الأرصدة الصافية',
'budget.categoriesLabel': 'فئات', 'budget.categoriesLabel': 'فئات',
"costs.you": "أنت", 'costs.you': 'أنت',
"costs.youShort": "أنت", 'costs.youShort': 'أنت',
"costs.youLower": "أنت", 'costs.youLower': 'أنت',
"costs.youOwe": "عليك", 'costs.youOwe': 'عليك',
"costs.youOweSub": "عليك أن تدفع للآخرين", 'costs.youOweSub': 'عليك أن تدفع للآخرين',
"costs.youreOwed": "لك", 'costs.youreOwed': 'لك',
"costs.youreOwedSub": "على الآخرين أن يدفعوا لك", 'costs.youreOwedSub': 'على الآخرين أن يدفعوا لك',
"costs.totalSpend": "إجمالي إنفاق الرحلة", 'costs.totalSpend': 'إجمالي إنفاق الرحلة',
"costs.totalSpendSub": "عبر جميع المسافرين", 'costs.totalSpendSub': 'عبر جميع المسافرين',
"costs.to": "إلى", 'costs.to': 'إلى',
"costs.from": "من", 'costs.from': 'من',
"costs.allSettled": "لقد سوّيت كل حساباتك", 'costs.allSettled': 'لقد سوّيت كل حساباتك',
"costs.nothingOwed": "لا شيء مستحق لك", 'costs.nothingOwed': 'لا شيء مستحق لك',
"costs.yourShare": "حصتك", 'costs.yourShare': 'حصتك',
"costs.youPaid": "أنت دفعت", 'costs.youPaid': 'أنت دفعت',
"costs.expenses": "المصروفات", 'costs.expenses': 'المصروفات',
"costs.entries": "{count} إدخالات", 'costs.entries': '{count} إدخالات',
"costs.searchPlaceholder": "ابحث في المصروفات…", 'costs.searchPlaceholder': 'ابحث في المصروفات…',
"costs.filter.all": "الكل", 'costs.filter.all': 'الكل',
"costs.filter.mine": "دفعتها أنا", 'costs.filter.mine': 'دفعتها أنا',
"costs.filter.owed": "مستحق لي", 'costs.filter.owed': 'مستحق لي',
"costs.addExpense": "إضافة مصروف", 'costs.addExpense': 'إضافة مصروف',
"costs.editExpense": "تعديل المصروف", 'costs.editExpense': 'تعديل المصروف',
"costs.noMatch": "لا توجد مصروفات تطابق بحثك.", 'costs.noMatch': 'لا توجد مصروفات تطابق بحثك.',
"costs.emptyText": "لا توجد مصروفات بعد. أضف أول مصروف لك.", 'costs.emptyText': 'لا توجد مصروفات بعد. أضف أول مصروف لك.',
"costs.spent": "تم إنفاق {amount}", 'costs.spent': 'تم إنفاق {amount}',
"costs.noDate": "بدون تاريخ", 'costs.noDate': 'بدون تاريخ',
"costs.noOnePaid": "لم يدفع أحد بعد", 'costs.noOnePaid': 'لم يدفع أحد بعد',
"costs.youLent": "أقرضت {amount}", 'costs.youLent': 'أقرضت {amount}',
"costs.youBorrowed": "اقترضت {amount}", 'costs.youBorrowed': 'اقترضت {amount}',
"costs.settleUp": "تسوية الحساب", 'costs.settleUp': 'تسوية الحساب',
"costs.history": "السجل", 'costs.history': 'السجل',
"costs.everyoneSquare": "الجميع متعادلون", 'costs.everyoneSquare': 'الجميع متعادلون',
"costs.nothingOutstanding": "لا توجد مدفوعات معلّقة الآن.", 'costs.nothingOutstanding': 'لا توجد مدفوعات معلّقة الآن.',
"costs.pay": "ادفع", 'costs.pay': 'ادفع',
"costs.pays": "يدفع", 'costs.pays': 'يدفع',
"costs.settle": "تسوية", 'costs.settle': 'تسوية',
"costs.balances": "الأرصدة", 'costs.balances': 'الأرصدة',
"costs.byCategory": "حسب الفئة", 'costs.byCategory': 'حسب الفئة',
"costs.noCategories": "لا توجد مصروفات بعد.", 'costs.noCategories': 'لا توجد مصروفات بعد.',
"costs.settleHistory": "سجل التسويات", 'costs.settleHistory': 'سجل التسويات',
"costs.noSettlements": "لا توجد مدفوعات مسوّاة بعد.", 'costs.noSettlements': 'لا توجد مدفوعات مسوّاة بعد.',
"costs.paymentsSettled": "تمت تسوية {count} مدفوعات", 'costs.paymentsSettled': 'تمت تسوية {count} مدفوعات',
"costs.paid": "مدفوع", 'costs.paid': 'مدفوع',
"costs.undo": "تراجع", 'costs.undo': 'تراجع',
"costs.whatFor": "لأجل ماذا كان؟", 'costs.whatFor': 'لأجل ماذا كان؟',
"costs.namePlaceholder": "مثل: عشاء، هدايا تذكارية، وقود…", 'costs.namePlaceholder': 'مثل: عشاء، هدايا تذكارية، وقود…',
"costs.totalAmount": "المبلغ الإجمالي", 'costs.totalAmount': 'المبلغ الإجمالي',
"costs.currency": "العملة", 'costs.currency': 'العملة',
"costs.day": "اليوم", 'costs.day': 'اليوم',
"costs.rateLabel": "1 {from} بـ {to}", 'costs.rateLabel': '1 {from} بـ {to}',
"costs.category": "الفئة", 'costs.category': 'الفئة',
"costs.whoPaid": "من دفع؟", 'costs.whoPaid': 'من دفع؟',
"costs.splitBetween": "تقسيم بالتساوي بين", 'costs.splitBetween': 'تقسيم بالتساوي بين',
"costs.pickSomeone": "اختر شخصًا واحدًا على الأقل للتقسيم معه.", 'costs.pickSomeone': 'اختر شخصًا واحدًا على الأقل للتقسيم معه.',
"costs.splitSummary": "تقسيم على {count} · {amount} لكل واحد", 'costs.splitSummary': 'تقسيم على {count} · {amount} لكل واحد',
"costs.cat.accommodation": "الإقامة", 'costs.cat.accommodation': 'الإقامة',
"costs.cat.food": "الطعام والشراب", 'costs.cat.food': 'الطعام والشراب',
"costs.cat.groceries": "البقالة", 'costs.cat.groceries': 'البقالة',
"costs.cat.transport": "النقل", 'costs.cat.transport': 'النقل',
"costs.cat.flights": "الرحلات الجوية", 'costs.cat.flights': 'الرحلات الجوية',
"costs.cat.activities": "الأنشطة", 'costs.cat.activities': 'الأنشطة',
"costs.cat.sightseeing": "معالم سياحية", 'costs.cat.sightseeing': 'معالم سياحية',
"costs.cat.shopping": "التسوق", 'costs.cat.shopping': 'التسوق',
"costs.cat.fees": "الرسوم والتذاكر", 'costs.cat.fees': 'الرسوم والتذاكر',
"costs.cat.health": "الصحة", 'costs.cat.health': 'الصحة',
"costs.cat.tips": "البقشيش", 'costs.cat.tips': 'البقشيش',
"costs.cat.other": "أخرى", 'costs.cat.other': 'أخرى',
"costs.daysCount": "{count} أيام", 'costs.daysCount': '{count} أيام',
"costs.travelers": "{count} مسافرين", 'costs.travelers': '{count} مسافرين',
"costs.liveRate": "سعر مباشر", 'costs.liveRate': 'سعر مباشر',
"costs.settleAll": "تسوية الكل", 'costs.settleAll': 'تسوية الكل',
'costs.payment': 'دفعة',
'costs.editPayment': 'تعديل الدفعة',
'costs.addPayment': 'إضافة دفعة',
'costs.unfinished': 'غير مكتمل',
'costs.unfinishedHint': 'في الإجمالي فقط — لم تتم التسوية بعد',
'costs.tapToInclude': 'اضغط للتضمين',
'costs.amount': 'المبلغ',
}; };
export default budget; export default budget;
+1 -2
View File
@@ -13,8 +13,7 @@ const categories: TranslationStrings = {
'categories.defaultName': 'فئة', 'categories.defaultName': 'فئة',
'categories.update': 'تحديث', 'categories.update': 'تحديث',
'categories.create': 'إنشاء', 'categories.create': 'إنشاء',
'categories.confirm.delete': 'categories.confirm.delete': 'حذف الفئة؟ لن يتم حذف الأماكن التابعة لهذه الفئة.',
'حذف الفئة؟ لن يتم حذف الأماكن التابعة لهذه الفئة.',
'categories.toast.loadError': 'فشل تحميل الفئات', 'categories.toast.loadError': 'فشل تحميل الفئات',
'categories.toast.nameRequired': 'يرجى إدخال اسم', 'categories.toast.nameRequired': 'يرجى إدخال اسم',
'categories.toast.updated': 'تم تحديث الفئة', 'categories.toast.updated': 'تم تحديث الفئة',
+4 -8
View File
@@ -20,8 +20,7 @@ const dashboard: TranslationStrings = {
'dashboard.timezoneCustomTzPlaceholder': 'مثال: Asia/Riyadh', 'dashboard.timezoneCustomTzPlaceholder': 'مثال: Asia/Riyadh',
'dashboard.timezoneCustomAdd': 'إضافة', 'dashboard.timezoneCustomAdd': 'إضافة',
'dashboard.timezoneCustomErrorEmpty': 'أدخل معرّف منطقة زمنية', 'dashboard.timezoneCustomErrorEmpty': 'أدخل معرّف منطقة زمنية',
'dashboard.timezoneCustomErrorInvalid': 'dashboard.timezoneCustomErrorInvalid': 'منطقة زمنية غير صالحة. استخدم صيغة مثل Asia/Riyadh',
'منطقة زمنية غير صالحة. استخدم صيغة مثل Asia/Riyadh',
'dashboard.timezoneCustomErrorDuplicate': 'مضافة بالفعل', 'dashboard.timezoneCustomErrorDuplicate': 'مضافة بالفعل',
'dashboard.emptyTitle': 'لا توجد رحلات بعد', 'dashboard.emptyTitle': 'لا توجد رحلات بعد',
'dashboard.emptyText': 'أنشئ رحلتك الأولى وابدأ التخطيط', 'dashboard.emptyText': 'أنشئ رحلتك الأولى وابدأ التخطيط',
@@ -55,8 +54,7 @@ const dashboard: TranslationStrings = {
'dashboard.toast.restoreError': 'فشل الاستعادة', 'dashboard.toast.restoreError': 'فشل الاستعادة',
'dashboard.toast.copied': 'تم نسخ الرحلة!', 'dashboard.toast.copied': 'تم نسخ الرحلة!',
'dashboard.toast.copyError': 'فشل نسخ الرحلة', 'dashboard.toast.copyError': 'فشل نسخ الرحلة',
'dashboard.confirm.delete': 'dashboard.confirm.delete': 'حذف الرحلة "{title}"؟ سيتم حذف جميع الأماكن والخطط نهائيًا.',
'حذف الرحلة "{title}"؟ سيتم حذف جميع الأماكن والخطط نهائيًا.',
'dashboard.editTrip': 'تعديل الرحلة', 'dashboard.editTrip': 'تعديل الرحلة',
'dashboard.createTrip': 'إنشاء رحلة جديدة', 'dashboard.createTrip': 'إنشاء رحلة جديدة',
'dashboard.tripTitle': 'العنوان', 'dashboard.tripTitle': 'العنوان',
@@ -66,10 +64,8 @@ const dashboard: TranslationStrings = {
'dashboard.startDate': 'تاريخ البداية', 'dashboard.startDate': 'تاريخ البداية',
'dashboard.endDate': 'تاريخ النهاية', 'dashboard.endDate': 'تاريخ النهاية',
'dashboard.dayCount': 'عدد الأيام', 'dashboard.dayCount': 'عدد الأيام',
'dashboard.dayCountHint': 'dashboard.dayCountHint': 'عدد الأيام المراد التخطيط لها عندما لا يتم تحديد تواريخ السفر.',
'عدد الأيام المراد التخطيط لها عندما لا يتم تحديد تواريخ السفر.', 'dashboard.noDateHint': 'لا يوجد تاريخ محدد. سيتم إنشاء 7 أيام افتراضية ويمكنك تغيير ذلك لاحقًا.',
'dashboard.noDateHint':
'لا يوجد تاريخ محدد. سيتم إنشاء 7 أيام افتراضية ويمكنك تغيير ذلك لاحقًا.',
'dashboard.coverImage': 'صورة الغلاف', 'dashboard.coverImage': 'صورة الغلاف',
'dashboard.addCoverImage': 'إضافة صورة غلاف', 'dashboard.addCoverImage': 'إضافة صورة غلاف',
'dashboard.addMembers': 'رفاق السفر', 'dashboard.addMembers': 'رفاق السفر',
+1 -2
View File
@@ -7,8 +7,7 @@ const day: TranslationStrings = {
'day.sunrise': 'شروق الشمس', 'day.sunrise': 'شروق الشمس',
'day.sunset': 'غروب الشمس', 'day.sunset': 'غروب الشمس',
'day.hourlyForecast': 'التوقعات بالساعة', 'day.hourlyForecast': 'التوقعات بالساعة',
'day.climateHint': 'day.climateHint': 'متوسطات تاريخية — التوقعات الفعلية متاحة خلال 16 يومًا من هذا التاريخ.',
'متوسطات تاريخية — التوقعات الفعلية متاحة خلال 16 يومًا من هذا التاريخ.',
'day.noWeather': 'لا تتوفر بيانات طقس. أضف مكانًا بإحداثيات.', 'day.noWeather': 'لا تتوفر بيانات طقس. أضف مكانًا بإحداثيات.',
'day.overview': 'ملخص اليوم', 'day.overview': 'ملخص اليوم',
'day.accommodation': 'الإقامة', 'day.accommodation': 'الإقامة',
+4 -8
View File
@@ -3,18 +3,14 @@ import type { TranslationStrings } from '../types';
const dayplan: TranslationStrings = { const dayplan: TranslationStrings = {
'dayplan.icsTooltip': 'تصدير التقويم (ICS)', 'dayplan.icsTooltip': 'تصدير التقويم (ICS)',
'dayplan.emptyDay': 'لا توجد أماكن مخططة لهذا اليوم', 'dayplan.emptyDay': 'لا توجد أماكن مخططة لهذا اليوم',
'dayplan.cannotReorderTransport': 'dayplan.cannotReorderTransport': 'لا يمكن إعادة ترتيب الحجوزات ذات الوقت الثابت',
'لا يمكن إعادة ترتيب الحجوزات ذات الوقت الثابت',
'dayplan.confirmRemoveTimeTitle': 'إزالة الوقت؟', 'dayplan.confirmRemoveTimeTitle': 'إزالة الوقت؟',
'dayplan.confirmRemoveTimeBody': 'dayplan.confirmRemoveTimeBody': 'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.',
'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.',
'dayplan.confirmRemoveTimeAction': 'إزالة الوقت ونقل', 'dayplan.confirmRemoveTimeAction': 'إزالة الوقت ونقل',
'dayplan.confirmDeleteNoteTitle': 'حذف الملاحظة؟', 'dayplan.confirmDeleteNoteTitle': 'حذف الملاحظة؟',
'dayplan.confirmDeleteNoteBody': 'سيتم حذف هذه الملاحظة نهائيًا.', 'dayplan.confirmDeleteNoteBody': 'سيتم حذف هذه الملاحظة نهائيًا.',
'dayplan.cannotDropOnTimed': 'dayplan.cannotDropOnTimed': 'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت',
'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت', 'dayplan.cannotBreakChronology': 'سيؤدي هذا إلى كسر الترتيب الزمني للعناصر والحجوزات المجدولة',
'dayplan.cannotBreakChronology':
'سيؤدي هذا إلى كسر الترتيب الزمني للعناصر والحجوزات المجدولة',
'dayplan.addNote': 'إضافة ملاحظة', 'dayplan.addNote': 'إضافة ملاحظة',
'dayplan.editNote': 'تعديل الملاحظة', 'dayplan.editNote': 'تعديل الملاحظة',
'dayplan.noteAdd': 'إضافة ملاحظة', 'dayplan.noteAdd': 'إضافة ملاحظة',
+1 -2
View File
@@ -55,8 +55,7 @@ const ar: NotificationLocale = {
body: 'تلقينا طلبًا لإعادة تعيين كلمة المرور لحسابك في TREK. انقر على الزر أدناه لتعيين كلمة مرور جديدة.', body: 'تلقينا طلبًا لإعادة تعيين كلمة المرور لحسابك في TREK. انقر على الزر أدناه لتعيين كلمة مرور جديدة.',
ctaIntro: 'إعادة تعيين كلمة المرور', ctaIntro: 'إعادة تعيين كلمة المرور',
expiry: 'تنتهي صلاحية هذا الرابط خلال 60 دقيقة.', expiry: 'تنتهي صلاحية هذا الرابط خلال 60 دقيقة.',
ignore: ignore: 'إذا لم تطلب هذا، يمكنك تجاهل هذه الرسالة — لن تتغير كلمة المرور الخاصة بك.',
'إذا لم تطلب هذا، يمكنك تجاهل هذه الرسالة — لن تتغير كلمة المرور الخاصة بك.',
}, },
}; };
+3 -6
View File
@@ -13,8 +13,7 @@ const files: TranslationStrings = {
'files.uploadError': 'فشل الرفع', 'files.uploadError': 'فشل الرفع',
'files.dropzone': 'أسقط الملفات هنا', 'files.dropzone': 'أسقط الملفات هنا',
'files.dropzoneHint': 'أو انقر للتصفح', 'files.dropzoneHint': 'أو انقر للتصفح',
'files.allowedTypes': 'files.allowedTypes': 'صور، PDF، DOC، DOCX، XLS، XLSX، TXT، CSV · حد أقصى 50 ميغابايت',
'صور، PDF، DOC، DOCX، XLS، XLSX، TXT، CSV · حد أقصى 50 ميغابايت',
'files.uploading': 'جارٍ الرفع...', 'files.uploading': 'جارٍ الرفع...',
'files.filterAll': 'الكل', 'files.filterAll': 'الكل',
'files.filterPdf': 'ملفات PDF', 'files.filterPdf': 'ملفات PDF',
@@ -52,10 +51,8 @@ const files: TranslationStrings = {
'files.toast.assigned': 'تم إسناد الملف', 'files.toast.assigned': 'تم إسناد الملف',
'files.toast.assignError': 'فشل الإسناد', 'files.toast.assignError': 'فشل الإسناد',
'files.toast.restoreError': 'فشلت الاستعادة', 'files.toast.restoreError': 'فشلت الاستعادة',
'files.confirm.permanentDelete': 'files.confirm.permanentDelete': 'حذف هذا الملف نهائيًا؟ لا يمكن التراجع عن ذلك.',
'حذف هذا الملف نهائيًا؟ لا يمكن التراجع عن ذلك.', 'files.confirm.emptyTrash': 'حذف جميع ملفات سلة المهملات نهائيًا؟ لا يمكن التراجع عن ذلك.',
'files.confirm.emptyTrash':
'حذف جميع ملفات سلة المهملات نهائيًا؟ لا يمكن التراجع عن ذلك.',
'files.noteLabel': 'ملاحظة', 'files.noteLabel': 'ملاحظة',
'files.notePlaceholder': 'أضف ملاحظة...', 'files.notePlaceholder': 'أضف ملاحظة...',
}; };
+13 -26
View File
@@ -10,14 +10,12 @@ const journey: TranslationStrings = {
'journey.detail.places': 'أماكن', 'journey.detail.places': 'أماكن',
'journey.skeletons.show': 'إظهار الاقتراحات', 'journey.skeletons.show': 'إظهار الاقتراحات',
'journey.skeletons.hide': 'إخفاء الاقتراحات', 'journey.skeletons.hide': 'إخفاء الاقتراحات',
'journey.editor.discardChangesConfirm': 'journey.editor.discardChangesConfirm': 'لديك تغييرات غير محفوظة. هل تريد تجاهلها؟',
'لديك تغييرات غير محفوظة. هل تريد تجاهلها؟',
'journey.editor.uploadFailed': 'فشل رفع الصور', 'journey.editor.uploadFailed': 'فشل رفع الصور',
'journey.editor.uploadPhotos': 'رفع صور', 'journey.editor.uploadPhotos': 'رفع صور',
'journey.editor.uploading': '...جارٍ الرفع', 'journey.editor.uploading': '...جارٍ الرفع',
'journey.editor.uploadingProgress': 'جارٍ الرفع {done}/{total}…', 'journey.editor.uploadingProgress': 'جارٍ الرفع {done}/{total}…',
'journey.editor.uploadPartialFailed': 'journey.editor.uploadPartialFailed': 'فشل رفع {failed} من {total} — احفظ مجدداً للمحاولة',
'فشل رفع {failed} من {total} — احفظ مجدداً للمحاولة',
'journey.editor.fromGallery': 'من المعرض', 'journey.editor.fromGallery': 'من المعرض',
'journey.editor.addAnother': 'إضافة آخر', 'journey.editor.addAnother': 'إضافة آخر',
'journey.editor.makeFirst': 'جعله الأول', 'journey.editor.makeFirst': 'جعله الأول',
@@ -33,8 +31,7 @@ const journey: TranslationStrings = {
'journey.settings.reopenJourney': 'استعادة الرحلة', 'journey.settings.reopenJourney': 'استعادة الرحلة',
'journey.settings.archived': 'تم أرشفة الرحلة', 'journey.settings.archived': 'تم أرشفة الرحلة',
'journey.settings.reopened': 'تمت إعادة فتح الرحلة', 'journey.settings.reopened': 'تمت إعادة فتح الرحلة',
'journey.settings.endDescription': 'journey.settings.endDescription': 'يخفي شارة البث المباشر. يمكنك إعادة الفتح في أي وقت.',
'يخفي شارة البث المباشر. يمكنك إعادة الفتح في أي وقت.',
'journey.settings.failedToDelete': 'فشل في الحذف', 'journey.settings.failedToDelete': 'فشل في الحذف',
'journey.entries.deleteTitle': 'حذف الإدخال', 'journey.entries.deleteTitle': 'حذف الإدخال',
'journey.photosUploaded': 'تم رفع {count} صورة', 'journey.photosUploaded': 'تم رفع {count} صورة',
@@ -68,8 +65,7 @@ const journey: TranslationStrings = {
'journey.notFound': 'Journey not found', // en-fallback 'journey.notFound': 'Journey not found', // en-fallback
'journey.photos': 'Photos', // en-fallback 'journey.photos': 'Photos', // en-fallback
'journey.timelineEmpty': 'No stops yet', // en-fallback 'journey.timelineEmpty': 'No stops yet', // en-fallback
'journey.timelineEmptyHint': 'journey.timelineEmptyHint': 'Add a check-in or write a journal entry to get started', // en-fallback
'Add a check-in or write a journal entry to get started', // en-fallback
'journey.status.draft': 'Draft', // en-fallback 'journey.status.draft': 'Draft', // en-fallback
'journey.status.active': 'Active', // en-fallback 'journey.status.active': 'Active', // en-fallback
'journey.status.completed': 'Completed', // en-fallback 'journey.status.completed': 'Completed', // en-fallback
@@ -94,30 +90,25 @@ const journey: TranslationStrings = {
'journey.editor.titlePlaceholder': 'Give this moment a name...', // en-fallback 'journey.editor.titlePlaceholder': 'Give this moment a name...', // en-fallback
'journey.editor.bodyPlaceholder': 'Tell the story of this day...', // en-fallback 'journey.editor.bodyPlaceholder': 'Tell the story of this day...', // en-fallback
'journey.editor.placePlaceholder': 'Location (optional)', // en-fallback 'journey.editor.placePlaceholder': 'Location (optional)', // en-fallback
'journey.editor.tagsPlaceholder': 'journey.editor.tagsPlaceholder': 'Tags: hidden gem, best meal, must revisit...', // en-fallback
'Tags: hidden gem, best meal, must revisit...', // en-fallback
'journey.visibility.private': 'Private', // en-fallback 'journey.visibility.private': 'Private', // en-fallback
'journey.visibility.shared': 'Shared', // en-fallback 'journey.visibility.shared': 'Shared', // en-fallback
'journey.visibility.public': 'Public', // en-fallback 'journey.visibility.public': 'Public', // en-fallback
'journey.emptyState.title': 'Your story starts here', // en-fallback 'journey.emptyState.title': 'Your story starts here', // en-fallback
'journey.emptyState.subtitle': 'journey.emptyState.subtitle': 'Check in at a place or write your first journal entry', // en-fallback
'Check in at a place or write your first journal entry', // en-fallback 'journey.frontpage.subtitle': "Turn your trips into stories you'll never forget", // en-fallback
'journey.frontpage.subtitle':
"Turn your trips into stories you'll never forget", // en-fallback
'journey.frontpage.createJourney': 'Create Journey', // en-fallback 'journey.frontpage.createJourney': 'Create Journey', // en-fallback
'journey.frontpage.activeJourney': 'Active Journey', // en-fallback 'journey.frontpage.activeJourney': 'Active Journey', // en-fallback
'journey.frontpage.allJourneys': 'All Journeys', // en-fallback 'journey.frontpage.allJourneys': 'All Journeys', // en-fallback
'journey.frontpage.journeys': 'journeys', // en-fallback 'journey.frontpage.journeys': 'journeys', // en-fallback
'journey.frontpage.createNew': 'Create a new Journey', // en-fallback 'journey.frontpage.createNew': 'Create a new Journey', // en-fallback
'journey.frontpage.createNewSub': 'journey.frontpage.createNewSub': 'Pick trips, write stories, share your adventures', // en-fallback
'Pick trips, write stories, share your adventures', // en-fallback
'journey.frontpage.live': 'Live', // en-fallback 'journey.frontpage.live': 'Live', // en-fallback
'journey.frontpage.synced': 'Synced', // en-fallback 'journey.frontpage.synced': 'Synced', // en-fallback
'journey.frontpage.continueWriting': 'Continue writing', // en-fallback 'journey.frontpage.continueWriting': 'Continue writing', // en-fallback
'journey.frontpage.updated': 'Updated {time}', // en-fallback 'journey.frontpage.updated': 'Updated {time}', // en-fallback
'journey.frontpage.suggestionLabel': 'Trip just ended', // en-fallback 'journey.frontpage.suggestionLabel': 'Trip just ended', // en-fallback
'journey.frontpage.suggestionText': 'journey.frontpage.suggestionText': 'Turn <strong>{title}</strong> into a Journey', // en-fallback
'Turn <strong>{title}</strong> into a Journey', // en-fallback
'journey.frontpage.dismiss': 'Dismiss', // en-fallback 'journey.frontpage.dismiss': 'Dismiss', // en-fallback
'journey.frontpage.journeyName': 'Journey Name', // en-fallback 'journey.frontpage.journeyName': 'Journey Name', // en-fallback
'journey.frontpage.namePlaceholder': 'e.g. Southeast Asia 2026', // en-fallback 'journey.frontpage.namePlaceholder': 'e.g. Southeast Asia 2026', // en-fallback
@@ -131,11 +122,9 @@ const journey: TranslationStrings = {
'journey.detail.newEntry': 'New Entry', // en-fallback 'journey.detail.newEntry': 'New Entry', // en-fallback
'journey.detail.editEntry': 'Edit Entry', // en-fallback 'journey.detail.editEntry': 'Edit Entry', // en-fallback
'journey.detail.noEntries': 'No entries yet', // en-fallback 'journey.detail.noEntries': 'No entries yet', // en-fallback
'journey.detail.noEntriesHint': 'journey.detail.noEntriesHint': 'Add a trip to get started with skeleton entries', // en-fallback
'Add a trip to get started with skeleton entries', // en-fallback
'journey.detail.noPhotos': 'No photos yet', // en-fallback 'journey.detail.noPhotos': 'No photos yet', // en-fallback
'journey.detail.noPhotosHint': 'journey.detail.noPhotosHint': 'Upload photos to entries or browse your Immich/Synology library', // en-fallback
'Upload photos to entries or browse your Immich/Synology library', // en-fallback
'journey.detail.journeyTab': 'Journey', // en-fallback 'journey.detail.journeyTab': 'Journey', // en-fallback
'journey.detail.journeyStats': 'Journey Stats', // en-fallback 'journey.detail.journeyStats': 'Journey Stats', // en-fallback
'journey.detail.syncedTrips': 'Synced Trips', // en-fallback 'journey.detail.syncedTrips': 'Synced Trips', // en-fallback
@@ -221,15 +210,13 @@ const journey: TranslationStrings = {
'journey.settings.subtitlePlaceholder': 'e.g. Thailand, Vietnam & Cambodia', // en-fallback 'journey.settings.subtitlePlaceholder': 'e.g. Thailand, Vietnam & Cambodia', // en-fallback
'journey.settings.delete': 'Delete', // en-fallback 'journey.settings.delete': 'Delete', // en-fallback
'journey.settings.deleteJourney': 'Delete Journey', // en-fallback 'journey.settings.deleteJourney': 'Delete Journey', // en-fallback
'journey.settings.deleteMessage': 'journey.settings.deleteMessage': 'Delete "{title}"? All entries and photos will be lost.', // en-fallback
'Delete "{title}"? All entries and photos will be lost.', // en-fallback
'journey.settings.saved': 'Settings saved', // en-fallback 'journey.settings.saved': 'Settings saved', // en-fallback
'journey.settings.saveFailed': 'Failed to save', // en-fallback 'journey.settings.saveFailed': 'Failed to save', // en-fallback
'journey.settings.coverUpdated': 'Cover updated', // en-fallback 'journey.settings.coverUpdated': 'Cover updated', // en-fallback
'journey.settings.coverFailed': 'Upload failed', // en-fallback 'journey.settings.coverFailed': 'Upload failed', // en-fallback
'journey.public.notFound': 'Not Found', // en-fallback 'journey.public.notFound': 'Not Found', // en-fallback
'journey.public.notFoundMessage': 'journey.public.notFoundMessage': "This journey doesn't exist or the link has expired.", // en-fallback
"This journey doesn't exist or the link has expired.", // en-fallback
'journey.public.readOnly': 'Read-only · Public Journey', // en-fallback 'journey.public.readOnly': 'Read-only · Public Journey', // en-fallback
'journey.public.tagline': 'Travel Resource & Exploration Kit', // en-fallback 'journey.public.tagline': 'Travel Resource & Exploration Kit', // en-fallback
'journey.public.sharedVia': 'Shared via', // en-fallback 'journey.public.sharedVia': 'Shared via', // en-fallback
+6 -12
View File
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
const login: TranslationStrings = { const login: TranslationStrings = {
'login.error': 'فشل تسجيل الدخول. يرجى التحقق من بياناتك.', 'login.error': 'فشل تسجيل الدخول. يرجى التحقق من بياناتك.',
'login.tagline': 'رحلاتك.\nخطتك.', 'login.tagline': 'رحلاتك.\nخطتك.',
'login.description': 'login.description': 'خطط لرحلاتك بشكل تعاوني مع خرائط تفاعلية وميزانيات ومزامنة لحظية.',
'خطط لرحلاتك بشكل تعاوني مع خرائط تفاعلية وميزانيات ومزامنة لحظية.',
'login.features.maps': 'خرائط تفاعلية', 'login.features.maps': 'خرائط تفاعلية',
'login.features.mapsDesc': 'Google Places ومسارات وتجميع', 'login.features.mapsDesc': 'Google Places ومسارات وتجميع',
'login.features.realtime': 'مزامنة فورية', 'login.features.realtime': 'مزامنة فورية',
@@ -43,8 +42,7 @@ const login: TranslationStrings = {
'login.oidc.invalidState': 'جلسة غير صالحة. حاول مرة أخرى.', 'login.oidc.invalidState': 'جلسة غير صالحة. حاول مرة أخرى.',
'login.demoFailed': 'فشل الدخول إلى العرض التجريبي', 'login.demoFailed': 'فشل الدخول إلى العرض التجريبي',
'login.oidcSignIn': 'تسجيل الدخول عبر {name}', 'login.oidcSignIn': 'تسجيل الدخول عبر {name}',
'login.oidcOnly': 'login.oidcOnly': 'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
'login.oidcLoggedOut': 'تم تسجيل خروجك. سجّل الدخول مجدداً عبر مزود SSO.', 'login.oidcLoggedOut': 'تم تسجيل خروجك. سجّل الدخول مجدداً عبر مزود SSO.',
'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل', 'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل',
'login.mfaTitle': 'المصادقة الثنائية', 'login.mfaTitle': 'المصادقة الثنائية',
@@ -75,18 +73,14 @@ const login: TranslationStrings = {
'login.passwordsDontMatch': 'كلمتا المرور غير متطابقتين', 'login.passwordsDontMatch': 'كلمتا المرور غير متطابقتين',
'login.mfaCode': 'رمز 2FA', 'login.mfaCode': 'رمز 2FA',
'login.resetPasswordTitle': 'ضبط كلمة مرور جديدة', 'login.resetPasswordTitle': 'ضبط كلمة مرور جديدة',
'login.resetPasswordBody': 'login.resetPasswordBody': 'اختر كلمة مرور قوية لم تستخدمها هنا من قبل. 8 أحرف على الأقل.',
'اختر كلمة مرور قوية لم تستخدمها هنا من قبل. 8 أحرف على الأقل.', 'login.resetPasswordMfaBody': 'أدخل رمز 2FA أو رمز النسخ الاحتياطي لإتمام إعادة التعيين.',
'login.resetPasswordMfaBody':
'أدخل رمز 2FA أو رمز النسخ الاحتياطي لإتمام إعادة التعيين.',
'login.resetPasswordSubmit': 'إعادة تعيين كلمة المرور', 'login.resetPasswordSubmit': 'إعادة تعيين كلمة المرور',
'login.resetPasswordVerify': 'تحقق وأعد التعيين', 'login.resetPasswordVerify': 'تحقق وأعد التعيين',
'login.resetPasswordSuccessTitle': 'تم تحديث كلمة المرور', 'login.resetPasswordSuccessTitle': 'تم تحديث كلمة المرور',
'login.resetPasswordSuccessBody': 'login.resetPasswordSuccessBody': 'يمكنك الآن تسجيل الدخول بكلمة المرور الجديدة.',
'يمكنك الآن تسجيل الدخول بكلمة المرور الجديدة.',
'login.resetPasswordInvalidLink': 'رابط إعادة تعيين غير صالح', 'login.resetPasswordInvalidLink': 'رابط إعادة تعيين غير صالح',
'login.resetPasswordInvalidLinkBody': 'login.resetPasswordInvalidLinkBody': 'هذا الرابط مفقود أو تالف. اطلب رابطًا جديدًا للمتابعة.',
'هذا الرابط مفقود أو تالف. اطلب رابطًا جديدًا للمتابعة.',
'login.resetPasswordFailed': 'فشلت إعادة التعيين. ربما انتهت صلاحية الرابط.', 'login.resetPasswordFailed': 'فشلت إعادة التعيين. ربما انتهت صلاحية الرابط.',
'login.emailPlaceholder': 'your@email.com', // en-fallback 'login.emailPlaceholder': 'your@email.com', // en-fallback
'login.passkey.signIn': 'تسجيل الدخول باستخدام مفتاح المرور', 'login.passkey.signIn': 'تسجيل الدخول باستخدام مفتاح المرور',
+4 -8
View File
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
const memories: TranslationStrings = { const memories: TranslationStrings = {
'memories.title': 'صور', 'memories.title': 'صور',
'memories.notConnected': 'Immich غير متصل', 'memories.notConnected': 'Immich غير متصل',
'memories.notConnectedHint': 'memories.notConnectedHint': 'قم بتوصيل Immich في الإعدادات لعرض صور رحلتك هنا.',
'قم بتوصيل Immich في الإعدادات لعرض صور رحلتك هنا.',
'memories.notConnectedMultipleHint': 'memories.notConnectedMultipleHint':
'قم بتوصيل أحد موفري الصور هؤلاء: {provider_names} في الإعدادات لتتمكن من إضافة صور إلى هذه الرحلة.', 'قم بتوصيل أحد موفري الصور هؤلاء: {provider_names} في الإعدادات لتتمكن من إضافة صور إلى هذه الرحلة.',
'memories.noDates': 'أضف تواريخ لرحلتك لتحميل الصور.', 'memories.noDates': 'أضف تواريخ لرحلتك لتحميل الصور.',
@@ -24,8 +23,7 @@ const memories: TranslationStrings = {
'memories.providerOTP': 'رمز MFA (إذا كان مفعلاً)', 'memories.providerOTP': 'رمز MFA (إذا كان مفعلاً)',
'memories.skipSSLVerification': 'تخطي التحقق من شهادة SSL', 'memories.skipSSLVerification': 'تخطي التحقق من شهادة SSL',
'memories.immichAutoUpload': 'نسخ صور الرحلة إلى Immich عند الرفع', 'memories.immichAutoUpload': 'نسخ صور الرحلة إلى Immich عند الرفع',
'memories.providerUrlHintSynology': 'memories.providerUrlHintSynology': 'أدرج مسار تطبيق Photos في URL، مثل https://nas:5001/photo',
'أدرج مسار تطبيق Photos في URL، مثل https://nas:5001/photo',
'memories.testConnection': 'اختبار الاتصال', 'memories.testConnection': 'اختبار الاتصال',
'memories.testShort': 'اختبار', 'memories.testShort': 'اختبار',
'memories.testFirst': 'اختبر الاتصال أولاً', 'memories.testFirst': 'اختبر الاتصال أولاً',
@@ -34,8 +32,7 @@ const memories: TranslationStrings = {
'memories.connectionSuccess': 'تم الاتصال بـ Immich', 'memories.connectionSuccess': 'تم الاتصال بـ Immich',
'memories.connectionError': 'تعذر الاتصال بـ Immich', 'memories.connectionError': 'تعذر الاتصال بـ Immich',
'memories.saved': 'تم حفظ إعدادات {provider_name}', 'memories.saved': 'تم حفظ إعدادات {provider_name}',
'memories.providerDisconnectedBanner': 'memories.providerDisconnectedBanner': 'اتصالك بـ {provider_name} مفقود. أعد الاتصال في الإعدادات لعرض الصور.',
'اتصالك بـ {provider_name} مفقود. أعد الاتصال في الإعدادات لعرض الصور.',
'memories.saveError': 'تعذّر حفظ إعدادات {provider_name}', 'memories.saveError': 'تعذّر حفظ إعدادات {provider_name}',
'memories.addPhotos': 'إضافة صور', 'memories.addPhotos': 'إضافة صور',
'memories.linkAlbum': 'ربط ألبوم', 'memories.linkAlbum': 'ربط ألبوم',
@@ -59,8 +56,7 @@ const memories: TranslationStrings = {
'memories.tripDates': 'تواريخ الرحلة', 'memories.tripDates': 'تواريخ الرحلة',
'memories.allPhotos': 'جميع الصور', 'memories.allPhotos': 'جميع الصور',
'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟', 'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟',
'memories.confirmShareHint': 'memories.confirmShareHint': '{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
'{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
'memories.confirmShareButton': 'مشاركة الصور', 'memories.confirmShareButton': 'مشاركة الصور',
'memories.error.loadAlbums': 'فشل تحميل الألبومات', 'memories.error.loadAlbums': 'فشل تحميل الألبومات',
'memories.error.linkAlbum': 'فشل ربط الألبوم', 'memories.error.linkAlbum': 'فشل ربط الألبوم',
+1 -2
View File
@@ -35,7 +35,6 @@ const notif: TranslationStrings = {
'notif.generic.title': 'إشعار', 'notif.generic.title': 'إشعار',
'notif.generic.text': 'لديك إشعار جديد', 'notif.generic.text': 'لديك إشعار جديد',
'notif.dev.unknown_event.title': '[DEV] حدث غير معروف', 'notif.dev.unknown_event.title': '[DEV] حدث غير معروف',
'notif.dev.unknown_event.text': 'notif.dev.unknown_event.text': 'نوع الحدث "{event}" غير مسجل في EVENT_NOTIFICATION_CONFIG',
'نوع الحدث "{event}" غير مسجل في EVENT_NOTIFICATION_CONFIG',
}; };
export default notif; export default notif;
+2 -4
View File
@@ -14,8 +14,7 @@ const notifications: TranslationStrings = {
'notifications.delete': 'حذف', 'notifications.delete': 'حذف',
'notifications.system': 'النظام', 'notifications.system': 'النظام',
'notifications.synologySessionCleared.title': 'تم قطع اتصال Synology Photos', 'notifications.synologySessionCleared.title': 'تم قطع اتصال Synology Photos',
'notifications.synologySessionCleared.text': 'notifications.synologySessionCleared.text': 'تغير خادمك أو حسابك — انتقل إلى الإعدادات لاختبار اتصالك مرة أخرى.',
'تغير خادمك أو حسابك — انتقل إلى الإعدادات لاختبار اتصالك مرة أخرى.',
'notifications.versionAvailable.title': 'تحديث متاح', 'notifications.versionAvailable.title': 'تحديث متاح',
'notifications.versionAvailable.text': 'TREK {version} متاح الآن.', 'notifications.versionAvailable.text': 'TREK {version} متاح الآن.',
'notifications.versionAvailable.button': 'عرض التفاصيل', 'notifications.versionAvailable.button': 'عرض التفاصيل',
@@ -29,8 +28,7 @@ const notifications: TranslationStrings = {
'notifications.test.navigateText': 'إشعار تجريبي للتنقل.', 'notifications.test.navigateText': 'إشعار تجريبي للتنقل.',
'notifications.test.goThere': 'اذهب إلى هناك', 'notifications.test.goThere': 'اذهب إلى هناك',
'notifications.test.adminTitle': 'إذاعة المسؤول', 'notifications.test.adminTitle': 'إذاعة المسؤول',
'notifications.test.adminText': 'notifications.test.adminText': 'أرسل {actor} إشعاراً تجريبياً لجميع المسؤولين.',
'أرسل {actor} إشعاراً تجريبياً لجميع المسؤولين.',
'notifications.test.tripTitle': 'نشر {actor} في رحلتك', 'notifications.test.tripTitle': 'نشر {actor} في رحلتك',
'notifications.test.tripText': 'إشعار تجريبي للرحلة "{trip}".', 'notifications.test.tripText': 'إشعار تجريبي للرحلة "{trip}".',
}; };
+28 -56
View File
@@ -13,40 +13,29 @@ const oauth: TranslationStrings = {
'oauth.scope.group.weather': 'الطقس', 'oauth.scope.group.weather': 'الطقس',
'oauth.scope.group.journey': 'مذكرة السفر', 'oauth.scope.group.journey': 'مذكرة السفر',
'oauth.scope.trips:read.label': 'عرض الرحلات وخطط السفر', 'oauth.scope.trips:read.label': 'عرض الرحلات وخطط السفر',
'oauth.scope.trips:read.description': 'oauth.scope.trips:read.description': 'قراءة الرحلات والأيام والملاحظات والأعضاء',
'قراءة الرحلات والأيام والملاحظات والأعضاء',
'oauth.scope.trips:write.label': 'تحرير الرحلات وخطط السفر', 'oauth.scope.trips:write.label': 'تحرير الرحلات وخطط السفر',
'oauth.scope.trips:write.description': 'oauth.scope.trips:write.description': 'إنشاء وتحديث الرحلات والأيام والملاحظات وإدارة الأعضاء',
'إنشاء وتحديث الرحلات والأيام والملاحظات وإدارة الأعضاء',
'oauth.scope.trips:delete.label': 'حذف الرحلات', 'oauth.scope.trips:delete.label': 'حذف الرحلات',
'oauth.scope.trips:delete.description': 'oauth.scope.trips:delete.description': 'حذف الرحلات بأكملها نهائياً — هذا الإجراء لا يمكن التراجع عنه',
'حذف الرحلات بأكملها نهائياً — هذا الإجراء لا يمكن التراجع عنه',
'oauth.scope.trips:share.label': 'إدارة روابط المشاركة', 'oauth.scope.trips:share.label': 'إدارة روابط المشاركة',
'oauth.scope.trips:share.description': 'oauth.scope.trips:share.description': 'إنشاء روابط مشاركة عامة وتحديثها وإلغاؤها',
'إنشاء روابط مشاركة عامة وتحديثها وإلغاؤها',
'oauth.scope.places:read.label': 'عرض الأماكن وبيانات الخريطة', 'oauth.scope.places:read.label': 'عرض الأماكن وبيانات الخريطة',
'oauth.scope.places:read.description': 'oauth.scope.places:read.description': 'قراءة الأماكن وتعيينات الأيام والعلامات والفئات',
'قراءة الأماكن وتعيينات الأيام والعلامات والفئات',
'oauth.scope.places:write.label': 'إدارة الأماكن', 'oauth.scope.places:write.label': 'إدارة الأماكن',
'oauth.scope.places:write.description': 'oauth.scope.places:write.description': 'إنشاء وتحديث وحذف الأماكن والتعيينات والعلامات',
'إنشاء وتحديث وحذف الأماكن والتعيينات والعلامات',
'oauth.scope.atlas:read.label': 'عرض Atlas', 'oauth.scope.atlas:read.label': 'عرض Atlas',
'oauth.scope.atlas:read.description': 'oauth.scope.atlas:read.description': 'قراءة الدول والمناطق المزارة وقائمة الأمنيات',
'قراءة الدول والمناطق المزارة وقائمة الأمنيات',
'oauth.scope.atlas:write.label': 'إدارة Atlas', 'oauth.scope.atlas:write.label': 'إدارة Atlas',
'oauth.scope.atlas:write.description': 'oauth.scope.atlas:write.description': 'تعليم الدول والمناطق كمزارة، وإدارة قائمة الأمنيات',
'تعليم الدول والمناطق كمزارة، وإدارة قائمة الأمنيات',
'oauth.scope.packing:read.label': 'عرض قوائم الأمتعة', 'oauth.scope.packing:read.label': 'عرض قوائم الأمتعة',
'oauth.scope.packing:read.description': 'oauth.scope.packing:read.description': 'قراءة عناصر الأمتعة والحقائب ومُسنَدي الفئات',
'قراءة عناصر الأمتعة والحقائب ومُسنَدي الفئات',
'oauth.scope.packing:write.label': 'إدارة قوائم الأمتعة', 'oauth.scope.packing:write.label': 'إدارة قوائم الأمتعة',
'oauth.scope.packing:write.description': 'oauth.scope.packing:write.description': 'إضافة وتحديث وحذف وتبديل وإعادة ترتيب عناصر الأمتعة والحقائب',
'إضافة وتحديث وحذف وتبديل وإعادة ترتيب عناصر الأمتعة والحقائب',
'oauth.scope.todos:read.label': 'عرض قوائم المهام', 'oauth.scope.todos:read.label': 'عرض قوائم المهام',
'oauth.scope.todos:read.description': 'قراءة مهام الرحلة ومُسنَدي الفئات', 'oauth.scope.todos:read.description': 'قراءة مهام الرحلة ومُسنَدي الفئات',
'oauth.scope.todos:write.label': 'إدارة قوائم المهام', 'oauth.scope.todos:write.label': 'إدارة قوائم المهام',
'oauth.scope.todos:write.description': 'oauth.scope.todos:write.description': 'إنشاء وتحديث وتبديل وحذف وإعادة ترتيب المهام',
'إنشاء وتحديث وتبديل وحذف وإعادة ترتيب المهام',
'oauth.scope.budget:read.label': 'عرض الميزانية', 'oauth.scope.budget:read.label': 'عرض الميزانية',
'oauth.scope.budget:read.description': 'قراءة بنود الميزانية وتفاصيل النفقات', 'oauth.scope.budget:read.description': 'قراءة بنود الميزانية وتفاصيل النفقات',
'oauth.scope.budget:write.label': 'إدارة الميزانية', 'oauth.scope.budget:write.label': 'إدارة الميزانية',
@@ -54,55 +43,40 @@ const oauth: TranslationStrings = {
'oauth.scope.reservations:read.label': 'عرض الحجوزات', 'oauth.scope.reservations:read.label': 'عرض الحجوزات',
'oauth.scope.reservations:read.description': 'قراءة الحجوزات وتفاصيل الإقامة', 'oauth.scope.reservations:read.description': 'قراءة الحجوزات وتفاصيل الإقامة',
'oauth.scope.reservations:write.label': 'إدارة الحجوزات', 'oauth.scope.reservations:write.label': 'إدارة الحجوزات',
'oauth.scope.reservations:write.description': 'oauth.scope.reservations:write.description': 'إنشاء وتحديث وحذف وإعادة ترتيب الحجوزات',
'إنشاء وتحديث وحذف وإعادة ترتيب الحجوزات',
'oauth.scope.collab:read.label': 'عرض التعاون', 'oauth.scope.collab:read.label': 'عرض التعاون',
'oauth.scope.collab:read.description': 'oauth.scope.collab:read.description': 'قراءة ملاحظات التعاون والاستطلاعات والرسائل',
'قراءة ملاحظات التعاون والاستطلاعات والرسائل',
'oauth.scope.collab:write.label': 'إدارة التعاون', 'oauth.scope.collab:write.label': 'إدارة التعاون',
'oauth.scope.collab:write.description': 'oauth.scope.collab:write.description': 'إنشاء وتحديث وحذف الملاحظات والاستطلاعات والرسائل التعاونية',
'إنشاء وتحديث وحذف الملاحظات والاستطلاعات والرسائل التعاونية',
'oauth.scope.notifications:read.label': 'عرض الإشعارات', 'oauth.scope.notifications:read.label': 'عرض الإشعارات',
'oauth.scope.notifications:read.description': 'oauth.scope.notifications:read.description': 'قراءة إشعارات التطبيق وأعداد غير المقروءة',
'قراءة إشعارات التطبيق وأعداد غير المقروءة',
'oauth.scope.notifications:write.label': 'إدارة الإشعارات', 'oauth.scope.notifications:write.label': 'إدارة الإشعارات',
'oauth.scope.notifications:write.description': 'oauth.scope.notifications:write.description': 'تعليم الإشعارات كمقروءة والرد عليها',
'تعليم الإشعارات كمقروءة والرد عليها',
'oauth.scope.vacay:read.label': 'عرض خطط الإجازة', 'oauth.scope.vacay:read.label': 'عرض خطط الإجازة',
'oauth.scope.vacay:read.description': 'oauth.scope.vacay:read.description': 'قراءة بيانات تخطيط الإجازة والإدخالات والإحصاءات',
'قراءة بيانات تخطيط الإجازة والإدخالات والإحصاءات',
'oauth.scope.vacay:write.label': 'إدارة خطط الإجازة', 'oauth.scope.vacay:write.label': 'إدارة خطط الإجازة',
'oauth.scope.vacay:write.description': 'oauth.scope.vacay:write.description': 'إنشاء وإدارة إدخالات الإجازة والعطلات وخطط الفريق',
'إنشاء وإدارة إدخالات الإجازة والعطلات وخطط الفريق',
'oauth.scope.geo:read.label': 'الخرائط والترميز الجغرافي', 'oauth.scope.geo:read.label': 'الخرائط والترميز الجغرافي',
'oauth.scope.geo:read.description': 'oauth.scope.geo:read.description': 'البحث عن مواقع وحل عناوين الخرائط والترميز الجغرافي العكسي للإحداثيات',
'البحث عن مواقع وحل عناوين الخرائط والترميز الجغرافي العكسي للإحداثيات',
'oauth.scope.weather:read.label': 'توقعات الطقس', 'oauth.scope.weather:read.label': 'توقعات الطقس',
'oauth.scope.weather:read.description': 'oauth.scope.weather:read.description': 'جلب توقعات الطقس لمواقع الرحلة وتواريخها',
'جلب توقعات الطقس لمواقع الرحلة وتواريخها',
'oauth.scope.journey:read.label': 'عرض مذكرات السفر', 'oauth.scope.journey:read.label': 'عرض مذكرات السفر',
'oauth.scope.journey:read.description': 'oauth.scope.journey:read.description': 'قراءة مذكرات السفر والمدخلات وقائمة المساهمين',
'قراءة مذكرات السفر والمدخلات وقائمة المساهمين',
'oauth.scope.journey:write.label': 'إدارة مذكرات السفر', 'oauth.scope.journey:write.label': 'إدارة مذكرات السفر',
'oauth.scope.journey:write.description': 'oauth.scope.journey:write.description': 'إنشاء مذكرات السفر وتحديثها وحذفها وإدخالاتها',
'إنشاء مذكرات السفر وتحديثها وحذفها وإدخالاتها',
'oauth.scope.journey:share.label': 'إدارة روابط مذكرات السفر', 'oauth.scope.journey:share.label': 'إدارة روابط مذكرات السفر',
'oauth.scope.journey:share.description': 'oauth.scope.journey:share.description': 'إنشاء روابط مشاركة عامة لمذكرات السفر وتحديثها وإلغاؤها',
'إنشاء روابط مشاركة عامة لمذكرات السفر وتحديثها وإلغاؤها',
'oauth.scope.group.atlas': 'Atlas', // en-fallback 'oauth.scope.group.atlas': 'Atlas', // en-fallback
'oauth.scope.group.geo': 'Geo', // en-fallback 'oauth.scope.group.geo': 'Geo', // en-fallback
'oauth.authorize.authorizing': 'Authorizing…', // en-fallback 'oauth.authorize.authorizing': 'Authorizing…', // en-fallback
'oauth.authorize.loading': 'Loading…', // en-fallback 'oauth.authorize.loading': 'Loading…', // en-fallback
'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback 'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback
'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback 'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback
'oauth.authorize.loginDescription': 'oauth.authorize.loginDescription': '{client} wants access to your TREK account. Please sign in first.', // en-fallback
'{client} wants access to your TREK account. Please sign in first.', // en-fallback
'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback 'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback
'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback 'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback
'oauth.authorize.requestDescription': 'oauth.authorize.requestDescription': 'This application is requesting access to your TREK account.', // en-fallback
'This application is requesting access to your TREK account.', // en-fallback 'oauth.authorize.trustNote': 'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.trustNote':
'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.selectScope': 'Select at least one scope', // en-fallback 'oauth.authorize.selectScope': 'Select at least one scope', // en-fallback
'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // en-fallback 'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // en-fallback
'oauth.authorize.approveManyScopes': 'Approve ({count} scopes)', // en-fallback 'oauth.authorize.approveManyScopes': 'Approve ({count} scopes)', // en-fallback
@@ -111,9 +85,7 @@ const oauth: TranslationStrings = {
'oauth.authorize.choosePermissions': 'Choose which permissions to grant', // en-fallback 'oauth.authorize.choosePermissions': 'Choose which permissions to grant', // en-fallback
'oauth.authorize.permissionsRequested': 'Permissions requested', // en-fallback 'oauth.authorize.permissionsRequested': 'Permissions requested', // en-fallback
'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback 'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback
'oauth.authorize.alwaysTool.listTrips': 'oauth.authorize.alwaysTool.listTrips': 'List your trips so the AI can discover trip IDs', // en-fallback
'List your trips so the AI can discover trip IDs', // en-fallback 'oauth.authorize.alwaysTool.getTripSummary': 'Read a trip overview needed to use any other tool', // en-fallback
'oauth.authorize.alwaysTool.getTripSummary':
'Read a trip overview needed to use any other tool', // en-fallback
}; };
export default oauth; export default oauth;
+1 -2
View File
@@ -7,8 +7,7 @@ const packing: TranslationStrings = {
'packing.importTitle': 'استيراد قائمة التعبئة', 'packing.importTitle': 'استيراد قائمة التعبئة',
'packing.importHint': 'packing.importHint':
'عنصر واحد لكل سطر. يمكن إضافة الفئة والكمية مفصولة بفاصلة أو فاصلة منقوطة أو علامة تبويب: الاسم، الفئة، الكمية', 'عنصر واحد لكل سطر. يمكن إضافة الفئة والكمية مفصولة بفاصلة أو فاصلة منقوطة أو علامة تبويب: الاسم، الفئة، الكمية',
'packing.importPlaceholder': 'packing.importPlaceholder': 'فرشاة أسنان\nواقي شمس، نظافة\nقمصان، ملابس، 5\nجواز سفر، مستندات',
'فرشاة أسنان\nواقي شمس، نظافة\nقمصان، ملابس، 5\nجواز سفر، مستندات',
'packing.importCsv': 'تحميل CSV/TXT', 'packing.importCsv': 'تحميل CSV/TXT',
'packing.importAction': 'استيراد {count}', 'packing.importAction': 'استيراد {count}',
'packing.importSuccess': 'تم استيراد {count} عنصر', 'packing.importSuccess': 'تم استيراد {count} عنصر',
+5 -10
View File
@@ -32,25 +32,20 @@ const perm: TranslationStrings = {
'perm.action.collab_edit': 'التعاون (ملاحظات، استطلاعات، دردشة)', 'perm.action.collab_edit': 'التعاون (ملاحظات، استطلاعات، دردشة)',
'perm.action.share_manage': 'إدارة روابط المشاركة', 'perm.action.share_manage': 'إدارة روابط المشاركة',
'perm.actionHint.trip_create': 'من يمكنه إنشاء رحلات جديدة', 'perm.actionHint.trip_create': 'من يمكنه إنشاء رحلات جديدة',
'perm.actionHint.trip_edit': 'perm.actionHint.trip_edit': 'من يمكنه تغيير اسم الرحلة والتواريخ والوصف والعملة',
'من يمكنه تغيير اسم الرحلة والتواريخ والوصف والعملة',
'perm.actionHint.trip_delete': 'من يمكنه حذف رحلة نهائياً', 'perm.actionHint.trip_delete': 'من يمكنه حذف رحلة نهائياً',
'perm.actionHint.trip_archive': 'من يمكنه أرشفة أو إلغاء أرشفة رحلة', 'perm.actionHint.trip_archive': 'من يمكنه أرشفة أو إلغاء أرشفة رحلة',
'perm.actionHint.trip_cover_upload': 'من يمكنه رفع أو تغيير صورة الغلاف', 'perm.actionHint.trip_cover_upload': 'من يمكنه رفع أو تغيير صورة الغلاف',
'perm.actionHint.member_manage': 'من يمكنه دعوة أو إزالة أعضاء الرحلة', 'perm.actionHint.member_manage': 'من يمكنه دعوة أو إزالة أعضاء الرحلة',
'perm.actionHint.file_upload': 'من يمكنه رفع ملفات إلى رحلة', 'perm.actionHint.file_upload': 'من يمكنه رفع ملفات إلى رحلة',
'perm.actionHint.file_edit': 'من يمكنه تعديل أوصاف الملفات والروابط', 'perm.actionHint.file_edit': 'من يمكنه تعديل أوصاف الملفات والروابط',
'perm.actionHint.file_delete': 'perm.actionHint.file_delete': 'من يمكنه نقل الملفات إلى سلة المهملات أو حذفها نهائياً',
'من يمكنه نقل الملفات إلى سلة المهملات أو حذفها نهائياً',
'perm.actionHint.place_edit': 'من يمكنه إضافة أو تعديل أو حذف الأماكن', 'perm.actionHint.place_edit': 'من يمكنه إضافة أو تعديل أو حذف الأماكن',
'perm.actionHint.day_edit': 'perm.actionHint.day_edit': 'من يمكنه تعديل الأيام وملاحظات الأيام وتعيينات الأماكن',
'من يمكنه تعديل الأيام وملاحظات الأيام وتعيينات الأماكن',
'perm.actionHint.reservation_edit': 'من يمكنه إنشاء أو تعديل أو حذف الحجوزات', 'perm.actionHint.reservation_edit': 'من يمكنه إنشاء أو تعديل أو حذف الحجوزات',
'perm.actionHint.budget_edit': 'perm.actionHint.budget_edit': 'من يمكنه إنشاء أو تعديل أو حذف عناصر الميزانية',
'من يمكنه إنشاء أو تعديل أو حذف عناصر الميزانية',
'perm.actionHint.packing_edit': 'من يمكنه إدارة عناصر التعبئة والحقائب', 'perm.actionHint.packing_edit': 'من يمكنه إدارة عناصر التعبئة والحقائب',
'perm.actionHint.collab_edit': 'perm.actionHint.collab_edit': 'من يمكنه إنشاء ملاحظات واستطلاعات وإرسال رسائل',
'من يمكنه إنشاء ملاحظات واستطلاعات وإرسال رسائل',
'perm.actionHint.share_manage': 'من يمكنه إنشاء أو حذف روابط المشاركة العامة', 'perm.actionHint.share_manage': 'من يمكنه إنشاء أو حذف روابط المشاركة العامة',
}; };
export default perm; export default perm;
+1 -2
View File
@@ -20,7 +20,6 @@ const photos: TranslationStrings = {
'photos.dayLabel': 'اليوم {number}', 'photos.dayLabel': 'اليوم {number}',
'photos.photoSelected': 'صورة محددة', 'photos.photoSelected': 'صورة محددة',
'photos.photosSelected': 'صور محددة', 'photos.photosSelected': 'صور محددة',
'photos.fileTypeHint': 'photos.fileTypeHint': 'JPG, PNG, WebP · الحد الأقصى 10 ميغابايت · حتى 30 صورة',
'JPG, PNG, WebP · الحد الأقصى 10 ميغابايت · حتى 30 صورة',
}; };
export default photos; export default photos;
+5 -10
View File
@@ -8,10 +8,8 @@ const places: TranslationStrings = {
'استورد ملفات .gpx أو .kml أو .kmz من أدوات مثل Google My Maps وGoogle Earth أو جهاز تتبع GPS.', 'استورد ملفات .gpx أو .kml أو .kmz من أدوات مثل Google My Maps وGoogle Earth أو جهاز تتبع GPS.',
'places.importFileDropHere': 'انقر لاختيار ملف أو اسحبه وأفلته هنا', 'places.importFileDropHere': 'انقر لاختيار ملف أو اسحبه وأفلته هنا',
'places.importFileDropActive': 'أفلت الملف للاختيار', 'places.importFileDropActive': 'أفلت الملف للاختيار',
'places.importFileUnsupported': 'places.importFileUnsupported': 'نوع الملف غير مدعوم. استخدم .gpx أو .kml أو .kmz.',
'نوع الملف غير مدعوم. استخدم .gpx أو .kml أو .kmz.', 'places.importFileTooLarge': 'الملف كبير جدًا. الحد الأقصى لحجم الرفع هو {maxMb} MB.',
'places.importFileTooLarge':
'الملف كبير جدًا. الحد الأقصى لحجم الرفع هو {maxMb} MB.',
'places.importFileError': 'فشل الاستيراد', 'places.importFileError': 'فشل الاستيراد',
'places.importAllSkipped': 'جميع الأماكن موجودة بالفعل في الرحلة.', 'places.importAllSkipped': 'جميع الأماكن موجودة بالفعل في الرحلة.',
'places.gpxImported': 'تم استيراد {count} مكان من GPX', 'places.gpxImported': 'تم استيراد {count} مكان من GPX',
@@ -29,16 +27,13 @@ const places: TranslationStrings = {
'places.kmlKmzImported': 'تم استيراد {count} مكان من KMZ/KML', 'places.kmlKmzImported': 'تم استيراد {count} مكان من KMZ/KML',
'places.urlResolved': 'تم استيراد المكان من الرابط', 'places.urlResolved': 'تم استيراد المكان من الرابط',
'places.importList': 'استيراد قائمة', 'places.importList': 'استيراد قائمة',
'places.kmlKmzSummaryValues': 'places.kmlKmzSummaryValues': 'علامات المواضع: {total} • تم الاستيراد: {created} • تم التجاوز: {skipped}',
'علامات المواضع: {total} • تم الاستيراد: {created} • تم التجاوز: {skipped}',
'places.importGoogleList': 'قائمة Google', 'places.importGoogleList': 'قائمة Google',
'places.importNaverList': 'قائمة Naver', 'places.importNaverList': 'قائمة Naver',
'places.googleListHint': 'places.googleListHint': 'الصق رابط قائمة Google Maps المشتركة لاستيراد جميع الأماكن.',
'الصق رابط قائمة Google Maps المشتركة لاستيراد جميع الأماكن.',
'places.googleListImported': 'تم استيراد {count} أماكن من "{list}"', 'places.googleListImported': 'تم استيراد {count} أماكن من "{list}"',
'places.googleListError': 'فشل استيراد قائمة Google Maps', 'places.googleListError': 'فشل استيراد قائمة Google Maps',
'places.naverListHint': 'places.naverListHint': 'الصق رابط قائمة Naver Maps مشتركة لاستيراد جميع الأماكن.',
'الصق رابط قائمة Naver Maps مشتركة لاستيراد جميع الأماكن.',
'places.naverListImported': 'تم استيراد {count} مكان من "{list}"', 'places.naverListImported': 'تم استيراد {count} مكان من "{list}"',
'places.naverListError': 'فشل استيراد قائمة Naver Maps', 'places.naverListError': 'فشل استيراد قائمة Naver Maps',
'places.viewDetails': 'عرض التفاصيل', 'places.viewDetails': 'عرض التفاصيل',
+1 -2
View File
@@ -33,8 +33,7 @@ const planner: TranslationStrings = {
'planner.resConfirmed': 'حجز مؤكد · ', 'planner.resConfirmed': 'حجز مؤكد · ',
'planner.notePlaceholder': 'ملاحظة…', 'planner.notePlaceholder': 'ملاحظة…',
'planner.noteTimePlaceholder': 'الوقت (اختياري)', 'planner.noteTimePlaceholder': 'الوقت (اختياري)',
'planner.noteExamplePlaceholder': 'planner.noteExamplePlaceholder': 'مثال: S3 الساعة 14:30 من المحطة المركزية، عبّارة من الرصيف 7، استراحة غداء…',
'مثال: S3 الساعة 14:30 من المحطة المركزية، عبّارة من الرصيف 7، استراحة غداء…',
'planner.totalCost': 'إجمالي التكلفة', 'planner.totalCost': 'إجمالي التكلفة',
'planner.searchPlaces': 'ابحث عن أماكن…', 'planner.searchPlaces': 'ابحث عن أماكن…',
'planner.allCategories': 'كل الفئات', 'planner.allCategories': 'كل الفئات',
+14 -28
View File
@@ -6,8 +6,7 @@ const reservations: TranslationStrings = {
'reservations.emptyHint': 'أضف حجوزات للرحلات الجوية والفنادق وغير ذلك', 'reservations.emptyHint': 'أضف حجوزات للرحلات الجوية والفنادق وغير ذلك',
'reservations.add': 'إضافة حجز', 'reservations.add': 'إضافة حجز',
'reservations.addManual': 'حجز يدوي', 'reservations.addManual': 'حجز يدوي',
'reservations.placeHint': 'reservations.placeHint': 'نصيحة: يُفضل إنشاء الحجوزات مباشرة من مكان لربطها بخطة اليوم.',
'نصيحة: يُفضل إنشاء الحجوزات مباشرة من مكان لربطها بخطة اليوم.',
'reservations.confirmed': 'مؤكد', 'reservations.confirmed': 'مؤكد',
'reservations.pending': 'قيد الانتظار', 'reservations.pending': 'قيد الانتظار',
'reservations.summary': '{confirmed} مؤكدة، {pending} قيد الانتظار', 'reservations.summary': '{confirmed} مؤكدة، {pending} قيد الانتظار',
@@ -33,8 +32,7 @@ const reservations: TranslationStrings = {
'reservations.layover.connection': 'رحلة متّصلة', 'reservations.layover.connection': 'رحلة متّصلة',
'reservations.layover.layover': 'توقف بيني', 'reservations.layover.layover': 'توقف بيني',
'reservations.needsReview': 'مراجعة', 'reservations.needsReview': 'مراجعة',
'reservations.needsReviewHint': 'reservations.needsReviewHint': 'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.',
'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.',
'reservations.searchLocation': 'ابحث عن محطة، ميناء، عنوان...', 'reservations.searchLocation': 'ابحث عن محطة، ميناء، عنوان...',
'reservations.meta.trainNumber': 'رقم القطار', 'reservations.meta.trainNumber': 'رقم القطار',
'reservations.meta.platform': 'المنصة', 'reservations.meta.platform': 'المنصة',
@@ -98,8 +96,7 @@ const reservations: TranslationStrings = {
'reservations.budgetCategory': 'فئة الميزانية', 'reservations.budgetCategory': 'فئة الميزانية',
'reservations.budgetCategoryPlaceholder': 'مثال: المواصلات، الإقامة', 'reservations.budgetCategoryPlaceholder': 'مثال: المواصلات، الإقامة',
'reservations.budgetCategoryAuto': 'تلقائي (حسب نوع الحجز)', 'reservations.budgetCategoryAuto': 'تلقائي (حسب نوع الحجز)',
'reservations.budgetHint': 'reservations.budgetHint': 'سيتم إنشاء إدخال في الميزانية تلقائيًا عند الحفظ.',
'سيتم إنشاء إدخال في الميزانية تلقائيًا عند الحفظ.',
'reservations.departureDate': 'المغادرة', 'reservations.departureDate': 'المغادرة',
'reservations.arrivalDate': 'الوصول', 'reservations.arrivalDate': 'الوصول',
'reservations.departureTime': 'وقت المغادرة', 'reservations.departureTime': 'وقت المغادرة',
@@ -120,8 +117,7 @@ const reservations: TranslationStrings = {
'reservations.span.start': 'البداية', 'reservations.span.start': 'البداية',
'reservations.span.end': 'النهاية', 'reservations.span.end': 'النهاية',
'reservations.span.ongoing': 'جارٍ', 'reservations.span.ongoing': 'جارٍ',
'reservations.validation.endBeforeStart': 'reservations.validation.endBeforeStart': 'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء',
'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء',
'reservations.addBooking': 'إضافة حجز', 'reservations.addBooking': 'إضافة حجز',
'reservations.import.title': 'استيراد تأكيدات الحجز', 'reservations.import.title': 'استيراد تأكيدات الحجز',
'reservations.import.cta': 'استيراد من ملف', 'reservations.import.cta': 'استيراد من ملف',
@@ -131,46 +127,36 @@ const reservations: TranslationStrings = {
'المقبول: EML، PDF، PKPass، HTML، TXT (بحد أقصى 10 ميغابايت لكل ملف، حتى 5 ملفات)', 'المقبول: EML، PDF، PKPass، HTML، TXT (بحد أقصى 10 ميغابايت لكل ملف، حتى 5 ملفات)',
'reservations.import.parsing': 'جارٍ معالجة الملفات…', 'reservations.import.parsing': 'جارٍ معالجة الملفات…',
'reservations.import.previewHeading': 'تم العثور على {count} حجز/حجوزات', 'reservations.import.previewHeading': 'تم العثور على {count} حجز/حجوزات',
'reservations.import.previewEmpty': 'reservations.import.previewEmpty': 'تعذّر استخراج أي حجوزات من الملفات المُحمَّلة.',
'تعذّر استخراج أي حجوزات من الملفات المُحمَّلة.',
'reservations.import.removeItem': 'إزالة', 'reservations.import.removeItem': 'إزالة',
'reservations.import.confirm': 'استيراد {count} حجز/حجوزات', 'reservations.import.confirm': 'استيراد {count} حجز/حجوزات',
'reservations.import.back': 'رجوع', 'reservations.import.back': 'رجوع',
'reservations.import.success': 'تم استيراد {count} حجز/حجوزات', 'reservations.import.success': 'تم استيراد {count} حجز/حجوزات',
'reservations.import.partialFailure': 'تم استيراد {created}، فشل {failed}', 'reservations.import.partialFailure': 'تم استيراد {created}، فشل {failed}',
'reservations.import.error': 'reservations.import.error': 'فشلت المعالجة. تأكد من أن الملف تأكيد حجز صالح.',
'فشلت المعالجة. تأكد من أن الملف تأكيد حجز صالح.', 'reservations.import.unavailable': 'استيراد الحجوزات غير متاح على هذا الخادم.',
'reservations.import.unavailable': 'reservations.import.unsupportedFormat': 'صيغة ملف غير مدعومة. استخدم EML أو PDF أو PKPass أو HTML أو TXT.',
'استيراد الحجوزات غير متاح على هذا الخادم.',
'reservations.import.unsupportedFormat':
'صيغة ملف غير مدعومة. استخدم EML أو PDF أو PKPass أو HTML أو TXT.',
'reservations.import.fileTooLarge': 'الملف "{name}" يتجاوز حد 10 ميغابايت.', 'reservations.import.fileTooLarge': 'الملف "{name}" يتجاوز حد 10 ميغابايت.',
'reservations.airtrail.title': 'استيراد من AirTrail', 'reservations.airtrail.title': 'استيراد من AirTrail',
'reservations.airtrail.cta': 'AirTrail', 'reservations.airtrail.cta': 'AirTrail',
'reservations.airtrail.synced': 'AirTrail', 'reservations.airtrail.synced': 'AirTrail',
'reservations.airtrail.syncedHint': 'reservations.airtrail.syncedHint': 'متزامن من AirTrail — تبقى التعديلات متزامنة في الاتجاهين.',
'متزامن من AirTrail — تبقى التعديلات متزامنة في الاتجاهين.',
'reservations.airtrail.notSynced': 'غير متزامن', 'reservations.airtrail.notSynced': 'غير متزامن',
'reservations.airtrail.notSyncedHint': 'reservations.airtrail.notSyncedHint': 'تمت إزالة هذه الرحلة في AirTrail ولم تعد متزامنة.',
'تمت إزالة هذه الرحلة في AirTrail ولم تعد متزامنة.',
'reservations.airtrail.loadError': 'تعذّر تحميل رحلاتك من AirTrail.', 'reservations.airtrail.loadError': 'تعذّر تحميل رحلاتك من AirTrail.',
'reservations.airtrail.imported': 'تم استيراد {count} رحلة/رحلات', 'reservations.airtrail.imported': 'تم استيراد {count} رحلة/رحلات',
'reservations.airtrail.skippedDuplicate': 'reservations.airtrail.skippedDuplicate': '{count} موجودة بالفعل في هذه الرحلة، تم تخطّيها',
'{count} موجودة بالفعل في هذه الرحلة، تم تخطّيها',
'reservations.airtrail.nothingImported': 'لا شيء لاستيراده.', 'reservations.airtrail.nothingImported': 'لا شيء لاستيراده.',
'reservations.airtrail.importError': 'reservations.airtrail.importError': 'فشل الاستيراد. يُرجى المحاولة مرة أخرى.',
'فشل الاستيراد. يُرجى المحاولة مرة أخرى.',
'reservations.airtrail.undo': 'استيراد من AirTrail', 'reservations.airtrail.undo': 'استيراد من AirTrail',
'reservations.airtrail.alreadyImported': 'مُستورَد', 'reservations.airtrail.alreadyImported': 'مُستورَد',
'reservations.airtrail.duringTrip': 'خلال هذه الرحلة', 'reservations.airtrail.duringTrip': 'خلال هذه الرحلة',
'reservations.airtrail.otherFlights': 'رحلات أخرى', 'reservations.airtrail.otherFlights': 'رحلات أخرى',
'reservations.airtrail.empty': 'reservations.airtrail.empty': 'لم يتم العثور على أي رحلات في حساب AirTrail الخاص بك.',
'لم يتم العثور على أي رحلات في حساب AirTrail الخاص بك.',
'reservations.airtrail.importCta': 'استيراد {count}', 'reservations.airtrail.importCta': 'استيراد {count}',
'reservations.costsLabel': 'Costs', 'reservations.costsLabel': 'Costs',
'reservations.createExpense': 'Create expense', 'reservations.createExpense': 'Create expense',
'reservations.createExpenseHint': 'reservations.createExpenseHint': 'Saves the booking, then opens the Costs editor.',
'Saves the booking, then opens the Costs editor.',
'reservations.linkedExpense': 'Linked expense', 'reservations.linkedExpense': 'Linked expense',
'reservations.removeExpense': 'Remove expense', 'reservations.removeExpense': 'Remove expense',
}; };
+36 -61
View File
@@ -15,8 +15,7 @@ const settings: TranslationStrings = {
'settings.mapDefaultHint': 'اتركه فارغًا لاستخدام OpenStreetMap افتراضيًا', 'settings.mapDefaultHint': 'اتركه فارغًا لاستخدام OpenStreetMap افتراضيًا',
'settings.mapHint': 'قالب URL لبلاطات الخريطة', 'settings.mapHint': 'قالب URL لبلاطات الخريطة',
'settings.mapProvider': 'مزود الخريطة', 'settings.mapProvider': 'مزود الخريطة',
'settings.mapProviderHint': 'settings.mapProviderHint': 'يؤثر على خرائط Trip Planner و Journey. يستخدم Atlas دائمًا Leaflet.',
'يؤثر على خرائط Trip Planner و Journey. يستخدم Atlas دائمًا Leaflet.',
'settings.mapLeafletSubtitle': '2D كلاسيكي، أي بلاطات نقطية', 'settings.mapLeafletSubtitle': '2D كلاسيكي، أي بلاطات نقطية',
'settings.mapMapboxSubtitle': 'بلاطات متجهية ومبانٍ ثلاثية الأبعاد وتضاريس', 'settings.mapMapboxSubtitle': 'بلاطات متجهية ومبانٍ ثلاثية الأبعاد وتضاريس',
'settings.mapExperimental': 'تجريبي', 'settings.mapExperimental': 'تجريبي',
@@ -25,14 +24,11 @@ const settings: TranslationStrings = {
'settings.mapMapboxTokenLink': 'mapbox.com ← رموز الوصول', 'settings.mapMapboxTokenLink': 'mapbox.com ← رموز الوصول',
'settings.mapStyle': 'نمط الخريطة', 'settings.mapStyle': 'نمط الخريطة',
'settings.mapStylePlaceholder': 'اختر نمط Mapbox', 'settings.mapStylePlaceholder': 'اختر نمط Mapbox',
'settings.mapStyleHint': 'settings.mapStyleHint': 'إعداد مسبق أو عنوان URL mapbox://styles/USER/ID خاص بك',
'إعداد مسبق أو عنوان URL mapbox://styles/USER/ID خاص بك',
'settings.map3dBuildings': 'مبانٍ ثلاثية الأبعاد وتضاريس', 'settings.map3dBuildings': 'مبانٍ ثلاثية الأبعاد وتضاريس',
'settings.map3dHint': 'settings.map3dHint': 'إمالة + مبانٍ ثلاثية الأبعاد حقيقية — يعمل مع كل نمط بما في ذلك الأقمار الصناعية.',
'إمالة + مبانٍ ثلاثية الأبعاد حقيقية — يعمل مع كل نمط بما في ذلك الأقمار الصناعية.',
'settings.mapHighQuality': 'وضع الجودة العالية', 'settings.mapHighQuality': 'وضع الجودة العالية',
'settings.mapHighQualityHint': 'settings.mapHighQualityHint': 'تحسين الحواف + إسقاط كروي لحواف أكثر حدة وعرض واقعي للعالم.',
'تحسين الحواف + إسقاط كروي لحواف أكثر حدة وعرض واقعي للعالم.',
'settings.mapHighQualityWarning': 'قد يؤثر على الأداء في الأجهزة الأقل قدرة.', 'settings.mapHighQualityWarning': 'قد يؤثر على الأداء في الأجهزة الأقل قدرة.',
'settings.mapTipLabel': 'نصيحة:', 'settings.mapTipLabel': 'نصيحة:',
'settings.mapTip': 'settings.mapTip':
@@ -57,8 +53,7 @@ const settings: TranslationStrings = {
'settings.temperature': 'وحدة الحرارة', 'settings.temperature': 'وحدة الحرارة',
'settings.timeFormat': 'تنسيق الوقت', 'settings.timeFormat': 'تنسيق الوقت',
'settings.bookingLabels': 'تسميات مسارات الحجوزات', 'settings.bookingLabels': 'تسميات مسارات الحجوزات',
'settings.bookingLabelsHint': 'settings.bookingLabelsHint': 'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
'settings.blurBookingCodes': 'إخفاء رموز الحجز', 'settings.blurBookingCodes': 'إخفاء رموز الحجز',
'settings.optimizeFromAccommodation': 'تحسين المسار انطلاقًا من مكان الإقامة', 'settings.optimizeFromAccommodation': 'تحسين المسار انطلاقًا من مكان الإقامة',
'settings.optimizeFromAccommodationHint': 'settings.optimizeFromAccommodationHint':
@@ -77,8 +72,7 @@ const settings: TranslationStrings = {
'settings.notificationPreferences.noChannels': 'settings.notificationPreferences.noChannels':
'لم يتم تكوين قنوات إشعارات. اطلب من المسؤول إعداد إشعارات البريد الإلكتروني أو webhook.', 'لم يتم تكوين قنوات إشعارات. اطلب من المسؤول إعداد إشعارات البريد الإلكتروني أو webhook.',
'settings.webhookUrl.label': 'رابط Webhook', 'settings.webhookUrl.label': 'رابط Webhook',
'settings.webhookUrl.hint': 'settings.webhookUrl.hint': 'أدخل رابط Webhook الخاص بـ Discord أو Slack أو المخصص لتلقي الإشعارات.',
'أدخل رابط Webhook الخاص بـ Discord أو Slack أو المخصص لتلقي الإشعارات.',
'settings.webhookUrl.saved': 'تم حفظ رابط Webhook', 'settings.webhookUrl.saved': 'تم حفظ رابط Webhook',
'settings.webhookUrl.test': 'اختبار', 'settings.webhookUrl.test': 'اختبار',
'settings.webhookUrl.testSuccess': 'تم إرسال Webhook الاختباري بنجاح', 'settings.webhookUrl.testSuccess': 'تم إرسال Webhook الاختباري بنجاح',
@@ -94,11 +88,9 @@ const settings: TranslationStrings = {
'settings.ntfyUrl.testSuccess': 'تم إرسال إشعار Ntfy التجريبي بنجاح', 'settings.ntfyUrl.testSuccess': 'تم إرسال إشعار Ntfy التجريبي بنجاح',
'settings.ntfyUrl.testFailed': 'فشل إشعار Ntfy التجريبي', 'settings.ntfyUrl.testFailed': 'فشل إشعار Ntfy التجريبي',
'settings.ntfyUrl.tokenCleared': 'تم مسح رمز الوصول', 'settings.ntfyUrl.tokenCleared': 'تم مسح رمز الوصول',
'settings.notificationsDisabled': 'settings.notificationsDisabled': 'الإشعارات غير مكوّنة. اطلب من المسؤول تفعيل إشعارات البريد الإلكتروني أو Webhook.',
'الإشعارات غير مكوّنة. اطلب من المسؤول تفعيل إشعارات البريد الإلكتروني أو Webhook.',
'settings.notificationsActive': 'القناة النشطة', 'settings.notificationsActive': 'القناة النشطة',
'settings.notificationsManagedByAdmin': 'settings.notificationsManagedByAdmin': 'يتم تكوين أحداث الإشعارات بواسطة المسؤول.',
'يتم تكوين أحداث الإشعارات بواسطة المسؤول.',
'settings.on': 'تشغيل', 'settings.on': 'تشغيل',
'settings.off': 'إيقاف', 'settings.off': 'إيقاف',
'settings.mcp.title': 'إعداد MCP', 'settings.mcp.title': 'إعداد MCP',
@@ -116,17 +108,14 @@ const settings: TranslationStrings = {
'settings.mcp.tokenCreatedAt': 'أُنشئ', 'settings.mcp.tokenCreatedAt': 'أُنشئ',
'settings.mcp.tokenUsedAt': 'استُخدم', 'settings.mcp.tokenUsedAt': 'استُخدم',
'settings.mcp.deleteTokenTitle': 'حذف الرمز', 'settings.mcp.deleteTokenTitle': 'حذف الرمز',
'settings.mcp.deleteTokenMessage': 'settings.mcp.deleteTokenMessage': 'سيتوقف هذا الرمز عن العمل فوراً. أي عميل MCP يستخدمه سيفقد الوصول.',
'سيتوقف هذا الرمز عن العمل فوراً. أي عميل MCP يستخدمه سيفقد الوصول.',
'settings.mcp.modal.createTitle': 'إنشاء رمز API', 'settings.mcp.modal.createTitle': 'إنشاء رمز API',
'settings.mcp.modal.tokenName': 'اسم الرمز', 'settings.mcp.modal.tokenName': 'اسم الرمز',
'settings.mcp.modal.tokenNamePlaceholder': 'settings.mcp.modal.tokenNamePlaceholder': 'مثال: Claude Desktop، حاسوب العمل',
'مثال: Claude Desktop، حاسوب العمل',
'settings.mcp.modal.creating': 'جارٍ الإنشاء…', 'settings.mcp.modal.creating': 'جارٍ الإنشاء…',
'settings.mcp.modal.create': 'إنشاء الرمز', 'settings.mcp.modal.create': 'إنشاء الرمز',
'settings.mcp.modal.createdTitle': 'تم إنشاء الرمز', 'settings.mcp.modal.createdTitle': 'تم إنشاء الرمز',
'settings.mcp.modal.createdWarning': 'settings.mcp.modal.createdWarning': 'سيُعرض هذا الرمز مرة واحدة فقط. انسخه واحفظه الآن — لا يمكن استرداده.',
'سيُعرض هذا الرمز مرة واحدة فقط. انسخه واحفظه الآن — لا يمكن استرداده.',
'settings.mcp.modal.done': 'تم', 'settings.mcp.modal.done': 'تم',
'settings.mcp.toast.created': 'تم إنشاء الرمز', 'settings.mcp.toast.created': 'تم إنشاء الرمز',
'settings.mcp.toast.createError': 'فشل إنشاء الرمز', 'settings.mcp.toast.createError': 'فشل إنشاء الرمز',
@@ -157,16 +146,13 @@ const settings: TranslationStrings = {
'settings.oauth.sessionExpires': 'تنتهي', 'settings.oauth.sessionExpires': 'تنتهي',
'settings.oauth.revoke': 'إلغاء', 'settings.oauth.revoke': 'إلغاء',
'settings.oauth.revokeSession': 'إلغاء الجلسة', 'settings.oauth.revokeSession': 'إلغاء الجلسة',
'settings.oauth.revokeSessionMessage': 'settings.oauth.revokeSessionMessage': 'سيؤدي هذا إلى إلغاء الوصول لهذه الجلسة OAuth فوراً.',
'سيؤدي هذا إلى إلغاء الوصول لهذه الجلسة OAuth فوراً.',
'settings.oauth.modal.createTitle': 'تسجيل عميل OAuth', 'settings.oauth.modal.createTitle': 'تسجيل عميل OAuth',
'settings.oauth.modal.presets': 'إعدادات سريعة', 'settings.oauth.modal.presets': 'إعدادات سريعة',
'settings.oauth.modal.clientName': 'اسم التطبيق', 'settings.oauth.modal.clientName': 'اسم التطبيق',
'settings.oauth.modal.clientNamePlaceholder': 'settings.oauth.modal.clientNamePlaceholder': 'مثال: Claude Web، تطبيق MCP الخاص بي',
'مثال: Claude Web، تطبيق MCP الخاص بي',
'settings.oauth.modal.redirectUris': 'عناوين URI لإعادة التوجيه', 'settings.oauth.modal.redirectUris': 'عناوين URI لإعادة التوجيه',
'settings.oauth.modal.redirectUrisHint': 'settings.oauth.modal.redirectUrisHint': 'عنوان URI واحد لكل سطر. يُطلب HTTPS (localhost مستثنى). يُطبق تطابق دقيق.',
'عنوان URI واحد لكل سطر. يُطلب HTTPS (localhost مستثنى). يُطبق تطابق دقيق.',
'settings.oauth.modal.scopes': 'النطاقات المسموح بها', 'settings.oauth.modal.scopes': 'النطاقات المسموح بها',
'settings.oauth.modal.scopesHint': 'settings.oauth.modal.scopesHint':
'list_trips وget_trip_summary متاحان دائماً — لا يُطلب نطاق. يساعدان الذكاء الاصطناعي في اكتشاف معرّفات الرحلات.', 'list_trips وget_trip_summary متاحان دائماً — لا يُطلب نطاق. يساعدان الذكاء الاصطناعي في اكتشاف معرّفات الرحلات.',
@@ -175,16 +161,14 @@ const settings: TranslationStrings = {
'settings.oauth.modal.creating': 'جارٍ التسجيل…', 'settings.oauth.modal.creating': 'جارٍ التسجيل…',
'settings.oauth.modal.create': 'تسجيل العميل', 'settings.oauth.modal.create': 'تسجيل العميل',
'settings.oauth.modal.createdTitle': 'تم تسجيل العميل', 'settings.oauth.modal.createdTitle': 'تم تسجيل العميل',
'settings.oauth.modal.createdWarning': 'settings.oauth.modal.createdWarning': 'يُعرض سر العميل مرة واحدة فقط. انسخه الآن — لا يمكن استرداده.',
'يُعرض سر العميل مرة واحدة فقط. انسخه الآن — لا يمكن استرداده.',
'settings.oauth.toast.createError': 'فشل تسجيل عميل OAuth', 'settings.oauth.toast.createError': 'فشل تسجيل عميل OAuth',
'settings.oauth.toast.deleted': 'تم حذف عميل OAuth', 'settings.oauth.toast.deleted': 'تم حذف عميل OAuth',
'settings.oauth.toast.deleteError': 'فشل حذف عميل OAuth', 'settings.oauth.toast.deleteError': 'فشل حذف عميل OAuth',
'settings.oauth.toast.revoked': 'تم إلغاء الجلسة', 'settings.oauth.toast.revoked': 'تم إلغاء الجلسة',
'settings.oauth.toast.revokeError': 'فشل إلغاء الجلسة', 'settings.oauth.toast.revokeError': 'فشل إلغاء الجلسة',
'settings.oauth.toast.rotateError': 'فشل تجديد سر العميل', 'settings.oauth.toast.rotateError': 'فشل تجديد سر العميل',
'settings.oauth.modal.machineClient': 'settings.oauth.modal.machineClient': 'عميل آلي (بدون تسجيل دخول عبر المتصفح)',
'عميل آلي (بدون تسجيل دخول عبر المتصفح)',
'settings.oauth.modal.machineClientHint': 'settings.oauth.modal.machineClientHint':
'استخدام منحة client_credentials — لا تحتاج إلى عناوين إعادة التوجيه. يُصدر الرمز المميز مباشرةً عبر client_id + client_secret ويعمل بصلاحياتك ضمن النطاقات المحددة.', 'استخدام منحة client_credentials — لا تحتاج إلى عناوين إعادة التوجيه. يُصدر الرمز المميز مباشرةً عبر client_id + client_secret ويعمل بصلاحياتك ضمن النطاقات المحددة.',
'settings.oauth.modal.machineClientUsage': 'settings.oauth.modal.machineClientUsage':
@@ -221,19 +205,15 @@ const settings: TranslationStrings = {
'settings.passwordRequired': 'أدخل كلمة المرور الحالية والجديدة', 'settings.passwordRequired': 'أدخل كلمة المرور الحالية والجديدة',
'settings.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل', 'settings.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل',
'settings.passwordMismatch': 'كلمتا المرور غير متطابقتين', 'settings.passwordMismatch': 'كلمتا المرور غير متطابقتين',
'settings.passwordWeak': 'settings.passwordWeak': 'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم ورمز خاص',
'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم ورمز خاص',
'settings.passwordChanged': 'تم تغيير كلمة المرور بنجاح', 'settings.passwordChanged': 'تم تغيير كلمة المرور بنجاح',
'settings.mustChangePassword': 'settings.mustChangePassword': 'يجب عليك تغيير كلمة المرور قبل المتابعة. يرجى تعيين كلمة مرور جديدة أدناه.',
'يجب عليك تغيير كلمة المرور قبل المتابعة. يرجى تعيين كلمة مرور جديدة أدناه.',
'settings.deleteAccount': 'حذف الحساب', 'settings.deleteAccount': 'حذف الحساب',
'settings.deleteAccountTitle': 'هل تريد حذف حسابك؟', 'settings.deleteAccountTitle': 'هل تريد حذف حسابك؟',
'settings.deleteAccountWarning': 'settings.deleteAccountWarning': 'سيتم حذف حسابك وجميع رحلاتك وأماكنك وملفاتك نهائيًا. لا يمكن التراجع عن ذلك.',
'سيتم حذف حسابك وجميع رحلاتك وأماكنك وملفاتك نهائيًا. لا يمكن التراجع عن ذلك.',
'settings.deleteAccountConfirm': 'حذف نهائي', 'settings.deleteAccountConfirm': 'حذف نهائي',
'settings.deleteBlockedTitle': 'الحذف غير ممكن', 'settings.deleteBlockedTitle': 'الحذف غير ممكن',
'settings.deleteBlockedMessage': 'settings.deleteBlockedMessage': 'أنت المسؤول الوحيد. قم بترقية مستخدم آخر إلى مسؤول قبل حذف حسابك.',
'أنت المسؤول الوحيد. قم بترقية مستخدم آخر إلى مسؤول قبل حذف حسابك.',
'settings.roleUser': 'مستخدم', 'settings.roleUser': 'مستخدم',
'settings.saveProfile': 'حفظ الملف الشخصي', 'settings.saveProfile': 'حفظ الملف الشخصي',
'settings.toast.mapSaved': 'تم حفظ إعدادات الخريطة', 'settings.toast.mapSaved': 'تم حفظ إعدادات الخريطة',
@@ -248,13 +228,10 @@ const settings: TranslationStrings = {
'settings.mfa.title': 'المصادقة الثنائية (2FA)', 'settings.mfa.title': 'المصادقة الثنائية (2FA)',
'settings.mfa.description': 'settings.mfa.description':
'تضيف خطوة ثانية عند تسجيل الدخول. استخدم تطبيق مصادقة (Google Authenticator، Authy، إلخ).', 'تضيف خطوة ثانية عند تسجيل الدخول. استخدم تطبيق مصادقة (Google Authenticator، Authy، إلخ).',
'settings.mfa.requiredByPolicy': 'settings.mfa.requiredByPolicy': 'المسؤول يتطلب المصادقة الثنائية. اضبط تطبيق المصادقة أدناه قبل المتابعة.',
'المسؤول يتطلب المصادقة الثنائية. اضبط تطبيق المصادقة أدناه قبل المتابعة.',
'settings.mfa.backupTitle': 'رموز النسخ الاحتياطي', 'settings.mfa.backupTitle': 'رموز النسخ الاحتياطي',
'settings.mfa.backupDescription': 'settings.mfa.backupDescription': 'استخدم هذه الرموز لمرة واحدة إذا فقدت الوصول إلى تطبيق المصادقة.',
'استخدم هذه الرموز لمرة واحدة إذا فقدت الوصول إلى تطبيق المصادقة.', 'settings.mfa.backupWarning': 'احفظ هذه الرموز الآن. كل رمز يمكن استخدامه مرة واحدة فقط.',
'settings.mfa.backupWarning':
'احفظ هذه الرموز الآن. كل رمز يمكن استخدامه مرة واحدة فقط.',
'settings.mfa.backupCopy': 'نسخ الرموز', 'settings.mfa.backupCopy': 'نسخ الرموز',
'settings.mfa.backupDownload': 'تنزيل TXT', 'settings.mfa.backupDownload': 'تنزيل TXT',
'settings.mfa.backupPrint': 'طباعة / PDF', 'settings.mfa.backupPrint': 'طباعة / PDF',
@@ -274,8 +251,7 @@ const settings: TranslationStrings = {
'settings.mfa.toastDisabled': 'تم تعطيل المصادقة الثنائية', 'settings.mfa.toastDisabled': 'تم تعطيل المصادقة الثنائية',
'settings.mfa.demoBlocked': 'غير متاح في الوضع التجريبي', 'settings.mfa.demoBlocked': 'غير متاح في الوضع التجريبي',
'settings.tabs.offline': 'Offline', // en-fallback 'settings.tabs.offline': 'Offline', // en-fallback
'settings.mapTemplatePlaceholder': 'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // en-fallback
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // en-fallback
'settings.notificationPreferences.email': 'Email', // en-fallback 'settings.notificationPreferences.email': 'Email', // en-fallback
'settings.notificationPreferences.webhook': 'Webhook', // en-fallback 'settings.notificationPreferences.webhook': 'Webhook', // en-fallback
'settings.notificationPreferences.inapp': 'In-App', // en-fallback 'settings.notificationPreferences.inapp': 'In-App', // en-fallback
@@ -283,16 +259,14 @@ const settings: TranslationStrings = {
'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', // en-fallback 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', // en-fallback
'settings.ntfyUrl.topicPlaceholder': 'my-trek-alerts', // en-fallback 'settings.ntfyUrl.topicPlaceholder': 'my-trek-alerts', // en-fallback
'settings.ntfyUrl.serverPlaceholder': 'https://ntfy.sh', // en-fallback 'settings.ntfyUrl.serverPlaceholder': 'https://ntfy.sh', // en-fallback
'settings.oauth.modal.redirectUrisPlaceholder': 'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth', // en-fallback
'https://your-app.com/callback\nhttps://your-app.com/auth', // en-fallback
'settings.about.supporter.tier.noReturnTicket': 'No Return Ticket', // en-fallback 'settings.about.supporter.tier.noReturnTicket': 'No Return Ticket', // en-fallback
'settings.about.supporter.tier.lostLuggageVip': 'Lost Luggage VIP', // en-fallback 'settings.about.supporter.tier.lostLuggageVip': 'Lost Luggage VIP', // en-fallback
'settings.about.supporter.tier.businessClassDreamer': 'settings.about.supporter.tier.businessClassDreamer': 'Business Class Dreamer', // en-fallback
'Business Class Dreamer', // en-fallback
'settings.about.supporter.tier.budgetTraveller': 'Budget Traveller', // en-fallback 'settings.about.supporter.tier.budgetTraveller': 'Budget Traveller', // en-fallback
'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate', // en-fallback 'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate', // en-fallback
"settings.currency": "Currency", 'settings.currency': 'Currency',
"settings.currencyHint": "All amounts in Costs are converted to and shown in this currency.", 'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
'settings.passkey.title': 'مفاتيح المرور', 'settings.passkey.title': 'مفاتيح المرور',
'settings.passkey.description': 'settings.passkey.description':
'سجّل الدخول بشكل أسرع وأكثر مقاومة للتصيّد باستخدام مفتاح مرور — ببصمة إصبعك أو وجهك أو رمز PIN أو مفتاح أمان مادي. تبقى كلمة المرور كنسخة احتياطية.', 'سجّل الدخول بشكل أسرع وأكثر مقاومة للتصيّد باستخدام مفتاح مرور — ببصمة إصبعك أو وجهك أو رمز PIN أو مفتاح أمان مادي. تبقى كلمة المرور كنسخة احتياطية.',
@@ -300,8 +274,7 @@ const settings: TranslationStrings = {
'مفاتيح المرور مفعّلة لكنها لم تُهيّأ بالكامل على هذا الخادم بعد. اطلب من المسؤول تعيين نطاق WebAuthn.', 'مفاتيح المرور مفعّلة لكنها لم تُهيّأ بالكامل على هذا الخادم بعد. اطلب من المسؤول تعيين نطاق WebAuthn.',
'settings.passkey.add': 'إضافة مفتاح مرور', 'settings.passkey.add': 'إضافة مفتاح مرور',
'settings.passkey.addTitle': 'إضافة مفتاح مرور', 'settings.passkey.addTitle': 'إضافة مفتاح مرور',
'settings.passkey.passwordPrompt': 'settings.passkey.passwordPrompt': 'أكّد كلمة المرور الحالية، ثم اتبع التعليمات على جهازك.',
'أكّد كلمة المرور الحالية، ثم اتبع التعليمات على جهازك.',
'settings.passkey.passwordRequired': 'كلمة المرور الحالية مطلوبة.', 'settings.passkey.passwordRequired': 'كلمة المرور الحالية مطلوبة.',
'settings.passkey.namePlaceholder': 'الاسم (اختياري، مثل "iPhone")', 'settings.passkey.namePlaceholder': 'الاسم (اختياري، مثل "iPhone")',
'settings.passkey.addedToast': 'تمت إضافة مفتاح المرور', 'settings.passkey.addedToast': 'تمت إضافة مفتاح المرور',
@@ -309,8 +282,7 @@ const settings: TranslationStrings = {
'settings.passkey.addError': 'تعذّرت إضافة مفتاح المرور', 'settings.passkey.addError': 'تعذّرت إضافة مفتاح المرور',
'settings.passkey.cancelled': 'تم إلغاء إعداد مفتاح المرور', 'settings.passkey.cancelled': 'تم إلغاء إعداد مفتاح المرور',
'settings.passkey.deleted': 'تمت إزالة مفتاح المرور', 'settings.passkey.deleted': 'تمت إزالة مفتاح المرور',
'settings.passkey.deleteConfirm': 'settings.passkey.deleteConfirm': 'إزالة مفتاح المرور هذا؟ أكّد بكلمة المرور الخاصة بك.',
'إزالة مفتاح المرور هذا؟ أكّد بكلمة المرور الخاصة بك.',
'settings.passkey.rename': 'إعادة التسمية', 'settings.passkey.rename': 'إعادة التسمية',
'settings.passkey.defaultName': 'مفتاح المرور', 'settings.passkey.defaultName': 'مفتاح المرور',
'settings.passkey.synced': 'متزامن', 'settings.passkey.synced': 'متزامن',
@@ -318,9 +290,11 @@ const settings: TranslationStrings = {
'settings.passkey.lastUsed': 'آخر استخدام', 'settings.passkey.lastUsed': 'آخر استخدام',
'settings.passkey.neverUsed': 'لم يُستخدم قط', 'settings.passkey.neverUsed': 'لم يُستخدم قط',
'settings.mapPoiPill': 'استكشاف الأماكن على الخريطة', 'settings.mapPoiPill': 'استكشاف الأماكن على الخريطة',
'settings.mapPoiPillHint': 'أظهر شريط فئات على خريطة الرحلة للعثور على المطاعم والفنادق والمزيد القريبة من OpenStreetMap.', 'settings.mapPoiPillHint':
'أظهر شريط فئات على خريطة الرحلة للعثور على المطاعم والفنادق والمزيد القريبة من OpenStreetMap.',
'settings.airtrail.title': 'AirTrail', 'settings.airtrail.title': 'AirTrail',
'settings.airtrail.hint': 'اربط نسخة AirTrail المُستضافة ذاتيًا لاستيراد الرحلات ومزامنتها. أنشئ مفتاح API في AirTrail ضمن الإعدادات ← الأمان.', 'settings.airtrail.hint':
'اربط نسخة AirTrail المُستضافة ذاتيًا لاستيراد الرحلات ومزامنتها. أنشئ مفتاح API في AirTrail ضمن الإعدادات ← الأمان.',
'settings.airtrail.url': 'رابط النسخة', 'settings.airtrail.url': 'رابط النسخة',
'settings.airtrail.apiKey': 'مفتاح API', 'settings.airtrail.apiKey': 'مفتاح API',
'settings.airtrail.apiKeyPlaceholder': 'مفتاح API من نوع Bearer', 'settings.airtrail.apiKeyPlaceholder': 'مفتاح API من نوع Bearer',
@@ -328,7 +302,8 @@ const settings: TranslationStrings = {
'settings.airtrail.allowInsecureTls': 'السماح بالشهادات الموقّعة ذاتيًا', 'settings.airtrail.allowInsecureTls': 'السماح بالشهادات الموقّعة ذاتيًا',
'settings.airtrail.allowInsecureTlsHint': 'فعّل هذا فقط لنسخة موثوقة على شبكتك الخاصة.', 'settings.airtrail.allowInsecureTlsHint': 'فعّل هذا فقط لنسخة موثوقة على شبكتك الخاصة.',
'settings.airtrail.writeBack': 'كتابة التغييرات إلى AirTrail', 'settings.airtrail.writeBack': 'كتابة التغييرات إلى AirTrail',
'settings.airtrail.writeBackHint': 'مُعطّل افتراضيًا: AirTrail هو مصدر الحقيقة وTREK يقرأ منه فقط. فعّله لإرسال التعديلات التي تجريها في TREK إلى AirTrail.', 'settings.airtrail.writeBackHint':
'مُعطّل افتراضيًا: AirTrail هو مصدر الحقيقة وTREK يقرأ منه فقط. فعّله لإرسال التعديلات التي تجريها في TREK إلى AirTrail.',
'settings.airtrail.connected': 'متصل', 'settings.airtrail.connected': 'متصل',
'settings.airtrail.notConnected': 'غير متصل', 'settings.airtrail.notConnected': 'غير متصل',
'settings.airtrail.toast.saved': 'تم حفظ اتصال AirTrail', 'settings.airtrail.toast.saved': 'تم حفظ اتصال AirTrail',
+5 -10
View File
@@ -5,22 +5,18 @@ const system_notice: TranslationStrings = {
'system_notice.v3_photos.body': 'system_notice.v3_photos.body':
'تمت إزالة تبويب ​**الصور**​ من مخطط الرحلة. صورك آمنة — لم يعدّل TREK مكتبتك على Immich أو Synology قطّ.\n\nتعيش الصور الآن في إضافة **Journey**. Journey اختيارية — إن لم تكن متاحة بعد، اطلب من المسؤول تفعيلها عبر Admin ← الإضافات.', 'تمت إزالة تبويب ​**الصور**​ من مخطط الرحلة. صورك آمنة — لم يعدّل TREK مكتبتك على Immich أو Synology قطّ.\n\nتعيش الصور الآن في إضافة **Journey**. Journey اختيارية — إن لم تكن متاحة بعد، اطلب من المسؤول تفعيلها عبر Admin ← الإضافات.',
'system_notice.v3_journey.title': 'تعرّف على Journey — مذكرة سفر', 'system_notice.v3_journey.title': 'تعرّف على Journey — مذكرة سفر',
'system_notice.v3_journey.body': 'system_notice.v3_journey.body': 'وثّق رحلاتك كقصص غنية بخطوط زمنية ومعارض صور وخرائط تفاعلية.',
'وثّق رحلاتك كقصص غنية بخطوط زمنية ومعارض صور وخرائط تفاعلية.',
'system_notice.v3_journey.cta_label': 'فتح Journey', 'system_notice.v3_journey.cta_label': 'فتح Journey',
'system_notice.v3_journey.highlight_timeline': 'جدول زمني يومي ومعرض', 'system_notice.v3_journey.highlight_timeline': 'جدول زمني يومي ومعرض',
'system_notice.v3_journey.highlight_photos': 'استيراد من Immich أو Synology', 'system_notice.v3_journey.highlight_photos': 'استيراد من Immich أو Synology',
'system_notice.v3_journey.highlight_share': 'مشاركة علنية — دون تسجيل دخول', 'system_notice.v3_journey.highlight_share': 'مشاركة علنية — دون تسجيل دخول',
'system_notice.v3_journey.highlight_export': 'تصدير كألبوم صور PDF', 'system_notice.v3_journey.highlight_export': 'تصدير كألبوم صور PDF',
'system_notice.v3_features.title': 'مزيد من مميزات 3.0', 'system_notice.v3_features.title': 'مزيد من مميزات 3.0',
'system_notice.v3_features.body': 'system_notice.v3_features.body': 'بعض الجديد الآخر الجدير بالمعرفة في هذا الإصدار.',
'بعض الجديد الآخر الجدير بالمعرفة في هذا الإصدار.', 'system_notice.v3_features.highlight_dashboard': 'إعادة تصميم لوحة التحكم mobile-first',
'system_notice.v3_features.highlight_dashboard':
'إعادة تصميم لوحة التحكم mobile-first',
'system_notice.v3_features.highlight_offline': 'وضع لا اتصال كامل كتطبيق PWA', 'system_notice.v3_features.highlight_offline': 'وضع لا اتصال كامل كتطبيق PWA',
'system_notice.v3_features.highlight_search': 'إكمال تلقائي في الوقت الفعلي', 'system_notice.v3_features.highlight_search': 'إكمال تلقائي في الوقت الفعلي',
'system_notice.v3_features.highlight_import': 'system_notice.v3_features.highlight_import': 'استيراد أماكن من ملفات KMZ/KML',
'استيراد أماكن من ملفات KMZ/KML',
'system_notice.v3_mcp.title': 'MCP: ترقية OAuth 2.1', 'system_notice.v3_mcp.title': 'MCP: ترقية OAuth 2.1',
'system_notice.v3_mcp.body': 'system_notice.v3_mcp.body':
'تمت إعادة تصميم تكامل MCP بالكامل. OAuth 2.1 هو الآن طريقة المصادقة الموصى بها. الرموز الثابتة (trek_…) مهملة وستُزال في إصدار مستقبلي.', 'تمت إعادة تصميم تكامل MCP بالكامل. OAuth 2.1 هو الآن طريقة المصادقة الموصى بها. الرموز الثابتة (trek_…) مهملة وستُزال في إصدار مستقبلي.',
@@ -31,8 +27,7 @@ const system_notice: TranslationStrings = {
'system_notice.v3_thankyou.title': 'كلمة شخصية مني', 'system_notice.v3_thankyou.title': 'كلمة شخصية مني',
'system_notice.v3_thankyou.body': 'system_notice.v3_thankyou.body':
'قبل أن تمضي — أريد أن أتوقف لحظة.\n\nبدأ TREK كمشروع جانبي بنيته لرحلاتي الخاصة. لم أتخيل يومًا أنه سيكبر ليصبح شيئًا يعتمد عليه 4,000 منكم لتخطيط مغامراتهم. كل نجمة، كل مشكلة، كل طلب ميزة — أقرأها جميعًا، وهي ما يبقيني مستمرًا في الليالي المتأخرة بين عمل بدوام كامل والجامعة.\n\nأريدكم أن تعرفوا: TREK سيبقى دائمًا مفتوح المصدر، دائمًا مستضافًا ذاتيًا، دائمًا ملككم. لا تتبع، لا اشتراكات، لا شروط خفية. مجرد أداة بناها شخص يحب السفر بقدر ما تحبونه.\n\nشكر خاص لـ [jubnl](https://github.com/jubnl) — لقد أصبحت متعاونًا رائعًا. الكثير مما يجعل الإصدار 3.0 عظيمًا يحمل بصماتك. شكرًا لإيمانك بهذا المشروع عندما كان لا يزال في بداياته.\n\nولكل واحد منكم ممن أبلغ عن خطأ، أو ترجم نصًا، أو شارك TREK مع صديق، أو ببساطة استخدمه لتخطيط رحلة — **شكرًا لكم**. أنتم السبب في وجود هذا.\n\nإلى المزيد من المغامرات معًا.\n\n— Maurice\n\n---\n\n[انضم إلى المجتمع على Discord](https://discord.gg/7Q6M6jDwzf)\n\nإذا جعل TREK رحلاتك أفضل، [فنجان قهوة صغير](https://ko-fi.com/mauriceboe) يبقي الأضواء مشتعلة.', 'قبل أن تمضي — أريد أن أتوقف لحظة.\n\nبدأ TREK كمشروع جانبي بنيته لرحلاتي الخاصة. لم أتخيل يومًا أنه سيكبر ليصبح شيئًا يعتمد عليه 4,000 منكم لتخطيط مغامراتهم. كل نجمة، كل مشكلة، كل طلب ميزة — أقرأها جميعًا، وهي ما يبقيني مستمرًا في الليالي المتأخرة بين عمل بدوام كامل والجامعة.\n\nأريدكم أن تعرفوا: TREK سيبقى دائمًا مفتوح المصدر، دائمًا مستضافًا ذاتيًا، دائمًا ملككم. لا تتبع، لا اشتراكات، لا شروط خفية. مجرد أداة بناها شخص يحب السفر بقدر ما تحبونه.\n\nشكر خاص لـ [jubnl](https://github.com/jubnl) — لقد أصبحت متعاونًا رائعًا. الكثير مما يجعل الإصدار 3.0 عظيمًا يحمل بصماتك. شكرًا لإيمانك بهذا المشروع عندما كان لا يزال في بداياته.\n\nولكل واحد منكم ممن أبلغ عن خطأ، أو ترجم نصًا، أو شارك TREK مع صديق، أو ببساطة استخدمه لتخطيط رحلة — **شكرًا لكم**. أنتم السبب في وجود هذا.\n\nإلى المزيد من المغامرات معًا.\n\n— Maurice\n\n---\n\n[انضم إلى المجتمع على Discord](https://discord.gg/7Q6M6jDwzf)\n\nإذا جعل TREK رحلاتك أفضل، [فنجان قهوة صغير](https://ko-fi.com/mauriceboe) يبقي الأضواء مشتعلة.',
'system_notice.v3014_whitespace_collision.title': 'system_notice.v3014_whitespace_collision.title': 'إجراء مطلوب: تعارض في حسابات المستخدمين',
'إجراء مطلوب: تعارض في حسابات المستخدمين',
'system_notice.v3014_whitespace_collision.body': 'system_notice.v3014_whitespace_collision.body':
'اكتشف ترقية 3.0.14 تعارضًا في أسماء مستخدمين أو بريد إلكتروني ناتجًا عن مسافات بيضاء في بداية أو نهاية القيم المخزنة. تمت إعادة تسمية الحسابات المتأثرة تلقائيًا. تحقق من سجلات الخادم بحثًا عن أسطر تبدأ بـ **[migration] WHITESPACE COLLISION** لتحديد الحسابات التي تحتاج إلى مراجعة.', 'اكتشف ترقية 3.0.14 تعارضًا في أسماء مستخدمين أو بريد إلكتروني ناتجًا عن مسافات بيضاء في بداية أو نهاية القيم المخزنة. تمت إعادة تسمية الحسابات المتأثرة تلقائيًا. تحقق من سجلات الخادم بحثًا عن أسطر تبدأ بـ **[migration] WHITESPACE COLLISION** لتحديد الحسابات التي تحتاج إلى مراجعة.',
'system_notice.welcome_v1.title': 'مرحبًا بك في TREK', 'system_notice.welcome_v1.title': 'مرحبًا بك في TREK',
+1 -1
View File
@@ -9,7 +9,7 @@ const trip: TranslationStrings = {
'trip.tabs.packingShort': 'تجهيز', 'trip.tabs.packingShort': 'تجهيز',
'trip.tabs.lists': 'القوائم', 'trip.tabs.lists': 'القوائم',
'trip.tabs.listsShort': 'القوائم', 'trip.tabs.listsShort': 'القوائم',
'trip.tabs.budget': "Costs", 'trip.tabs.budget': 'Costs',
'trip.tabs.files': 'الملفات', 'trip.tabs.files': 'الملفات',
'trip.loading': 'جارٍ تحميل الرحلة...', 'trip.loading': 'جارٍ تحميل الرحلة...',
'trip.loadingPhotos': 'جارٍ تحميل صور الأماكن...', 'trip.loadingPhotos': 'جارٍ تحميل صور الأماكن...',
+1 -2
View File
@@ -11,7 +11,6 @@ const trips: TranslationStrings = {
'trips.reminderDays': 'أيام', 'trips.reminderDays': 'أيام',
'trips.reminderCustom': 'مخصص', 'trips.reminderCustom': 'مخصص',
'trips.reminderDaysBefore': 'أيام قبل المغادرة', 'trips.reminderDaysBefore': 'أيام قبل المغادرة',
'trips.reminderDisabledHint': 'trips.reminderDisabledHint': 'تذكيرات الرحلة معطلة. قم بتفعيلها من الإدارة > الإعدادات > الإشعارات.',
'تذكيرات الرحلة معطلة. قم بتفعيلها من الإدارة > الإعدادات > الإشعارات.',
}; };
export default trips; export default trips;
+3 -6
View File
@@ -8,8 +8,7 @@ const vacay: TranslationStrings = {
'vacay.addPrevYear': 'إضافة السنة السابقة', 'vacay.addPrevYear': 'إضافة السنة السابقة',
'vacay.removeYear': 'إزالة السنة', 'vacay.removeYear': 'إزالة السنة',
'vacay.removeYearConfirm': 'إزالة {year}؟', 'vacay.removeYearConfirm': 'إزالة {year}؟',
'vacay.removeYearHint': 'vacay.removeYearHint': 'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.',
'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.',
'vacay.remove': 'إزالة', 'vacay.remove': 'إزالة',
'vacay.persons': 'الأشخاص', 'vacay.persons': 'الأشخاص',
'vacay.noPersons': 'لم تتم إضافة أشخاص بعد', 'vacay.noPersons': 'لم تتم إضافة أشخاص بعد',
@@ -57,8 +56,7 @@ const vacay: TranslationStrings = {
'vacay.weekStart': 'يبدأ الأسبوع في', 'vacay.weekStart': 'يبدأ الأسبوع في',
'vacay.weekStartHint': 'اختر ما إذا كان الأسبوع يبدأ يوم الاثنين أو الأحد', 'vacay.weekStartHint': 'اختر ما إذا كان الأسبوع يبدأ يوم الاثنين أو الأحد',
'vacay.carryOver': 'الترحيل', 'vacay.carryOver': 'الترحيل',
'vacay.carryOverHint': 'vacay.carryOverHint': 'ترحيل أيام الإجازة المتبقية تلقائيًا إلى السنة التالية',
'ترحيل أيام الإجازة المتبقية تلقائيًا إلى السنة التالية',
'vacay.sharing': 'المشاركة', 'vacay.sharing': 'المشاركة',
'vacay.sharingHint': 'شارك خطة إجازاتك مع مستخدمي TREK الآخرين', 'vacay.sharingHint': 'شارك خطة إجازاتك مع مستخدمي TREK الآخرين',
'vacay.owner': 'المالك', 'vacay.owner': 'المالك',
@@ -90,7 +88,6 @@ const vacay: TranslationStrings = {
'vacay.fuseInfo2': 'يمكن لكلا الطرفين إنشاء وتعديل الإدخالات لبعضهما البعض.', 'vacay.fuseInfo2': 'يمكن لكلا الطرفين إنشاء وتعديل الإدخالات لبعضهما البعض.',
'vacay.fuseInfo3': 'يمكن لكلا الطرفين حذف الإدخالات وتغيير مستحقات الإجازة.', 'vacay.fuseInfo3': 'يمكن لكلا الطرفين حذف الإدخالات وتغيير مستحقات الإجازة.',
'vacay.fuseInfo4': 'تتم مشاركة الإعدادات مثل العطل الرسمية وعطل الشركة.', 'vacay.fuseInfo4': 'تتم مشاركة الإعدادات مثل العطل الرسمية وعطل الشركة.',
'vacay.fuseInfo5': 'vacay.fuseInfo5': 'يمكن فك الدمج في أي وقت من قبل أي طرف. ستبقى إدخالاتك محفوظة.',
'يمكن فك الدمج في أي وقت من قبل أي طرف. ستبقى إدخالاتك محفوظة.',
}; };
export default vacay; export default vacay;
+52 -98
View File
@@ -2,22 +2,19 @@ import type { TranslationStrings } from '../types';
const admin: TranslationStrings = { const admin: TranslationStrings = {
'admin.notifications.title': 'Notificações', 'admin.notifications.title': 'Notificações',
'admin.notifications.hint': 'admin.notifications.hint': 'Escolha um canal de notificação. Apenas um pode estar ativo por vez.',
'Escolha um canal de notificação. Apenas um pode estar ativo por vez.',
'admin.notifications.none': 'Desativado', 'admin.notifications.none': 'Desativado',
'admin.notifications.email': 'E-mail (SMTP)', 'admin.notifications.email': 'E-mail (SMTP)',
'admin.notifications.webhook': 'Webhook', 'admin.notifications.webhook': 'Webhook',
'admin.notifications.save': 'Salvar configurações de notificação', 'admin.notifications.save': 'Salvar configurações de notificação',
'admin.notifications.saved': 'Configurações de notificação salvas', 'admin.notifications.saved': 'Configurações de notificação salvas',
'admin.notifications.testWebhook': 'Enviar webhook de teste', 'admin.notifications.testWebhook': 'Enviar webhook de teste',
'admin.notifications.testWebhookSuccess': 'admin.notifications.testWebhookSuccess': 'Webhook de teste enviado com sucesso',
'Webhook de teste enviado com sucesso',
'admin.notifications.testWebhookFailed': 'Falha ao enviar webhook de teste', 'admin.notifications.testWebhookFailed': 'Falha ao enviar webhook de teste',
'admin.smtp.title': 'E-mail e notificações', 'admin.smtp.title': 'E-mail e notificações',
'admin.smtp.hint': 'Configuração SMTP para envio de notificações por e-mail.', 'admin.smtp.hint': 'Configuração SMTP para envio de notificações por e-mail.',
'admin.smtp.testButton': 'Enviar e-mail de teste', 'admin.smtp.testButton': 'Enviar e-mail de teste',
'admin.webhook.hint': 'admin.webhook.hint': 'Enviar notificações para um webhook externo (Discord, Slack, etc.).',
'Enviar notificações para um webhook externo (Discord, Slack, etc.).',
'admin.smtp.testSuccess': 'E-mail de teste enviado com sucesso', 'admin.smtp.testSuccess': 'E-mail de teste enviado com sucesso',
'admin.smtp.testFailed': 'Falha ao enviar e-mail de teste', 'admin.smtp.testFailed': 'Falha ao enviar e-mail de teste',
'admin.title': 'Administração', 'admin.title': 'Administração',
@@ -40,8 +37,7 @@ const admin: TranslationStrings = {
'admin.editUser': 'Editar usuário', 'admin.editUser': 'Editar usuário',
'admin.newPassword': 'Nova senha', 'admin.newPassword': 'Nova senha',
'admin.newPasswordHint': 'Deixe em branco para manter a senha atual', 'admin.newPasswordHint': 'Deixe em branco para manter a senha atual',
'admin.deleteUser': 'admin.deleteUser': 'Excluir o usuário "{name}"? Todas as viagens serão excluídas permanentemente.',
'Excluir o usuário "{name}"? Todas as viagens serão excluídas permanentemente.',
'admin.deleteUserTitle': 'Excluir usuário', 'admin.deleteUserTitle': 'Excluir usuário',
'admin.newPasswordPlaceholder': 'Digite a nova senha…', 'admin.newPasswordPlaceholder': 'Digite a nova senha…',
'admin.toast.loadError': 'Falha ao carregar dados do admin', 'admin.toast.loadError': 'Falha ao carregar dados do admin',
@@ -52,8 +48,7 @@ const admin: TranslationStrings = {
'admin.toast.cannotDeleteSelf': 'Não é possível excluir a própria conta', 'admin.toast.cannotDeleteSelf': 'Não é possível excluir a própria conta',
'admin.toast.userCreated': 'Usuário criado', 'admin.toast.userCreated': 'Usuário criado',
'admin.toast.createError': 'Falha ao criar usuário', 'admin.toast.createError': 'Falha ao criar usuário',
'admin.toast.fieldsRequired': 'admin.toast.fieldsRequired': 'Nome de usuário, e-mail e senha são obrigatórios',
'Nome de usuário, e-mail e senha são obrigatórios',
'admin.createUser': 'Criar usuário', 'admin.createUser': 'Criar usuário',
'admin.invite.title': 'Links de convite', 'admin.invite.title': 'Links de convite',
'admin.invite.subtitle': 'Crie links de cadastro de uso único', 'admin.invite.subtitle': 'Crie links de cadastro de uso único',
@@ -80,51 +75,40 @@ const admin: TranslationStrings = {
'admin.passwordLogin': 'Password Login', 'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password', 'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration', 'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login', 'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO', 'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning', 'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'admin.envOverrideHint':
'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.', 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled', 'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Exigir autenticação em dois fatores (2FA)', 'admin.requireMfa': 'Exigir autenticação em dois fatores (2FA)',
'admin.requireMfaHint': 'admin.requireMfaHint': 'Usuários sem 2FA precisam concluir a configuração em Configurações antes de usar o app.',
'Usuários sem 2FA precisam concluir a configuração em Configurações antes de usar o app.',
'admin.apiKeys': 'Chaves de API', 'admin.apiKeys': 'Chaves de API',
'admin.apiKeysHint': 'admin.apiKeysHint': 'Opcional. Habilita dados estendidos de lugares, como fotos e clima.',
'Opcional. Habilita dados estendidos de lugares, como fotos e clima.',
'admin.mapsKey': 'Chave da API Google Maps', 'admin.mapsKey': 'Chave da API Google Maps',
'admin.mapsKeyHint': 'admin.mapsKeyHint': 'Necessária para busca de lugares. Obtenha em console.cloud.google.com',
'Necessária para busca de lugares. Obtenha em console.cloud.google.com',
'admin.mapsKeyHintLong': 'admin.mapsKeyHintLong':
'Sem chave de API, o OpenStreetMap é usado na busca. Com uma chave Google, também podem ser carregadas fotos, avaliações e horários. Obtenha em console.cloud.google.com.', 'Sem chave de API, o OpenStreetMap é usado na busca. Com uma chave Google, também podem ser carregadas fotos, avaliações e horários. Obtenha em console.cloud.google.com.',
'admin.recommended': 'Recomendado', 'admin.recommended': 'Recomendado',
'admin.weatherKey': 'Chave OpenWeatherMap', 'admin.weatherKey': 'Chave OpenWeatherMap',
'admin.weatherKeyHint': 'admin.weatherKeyHint': 'Para dados meteorológicos. Grátis em openweathermap.org',
'Para dados meteorológicos. Grátis em openweathermap.org',
'admin.validateKey': 'Testar', 'admin.validateKey': 'Testar',
'admin.keyValid': 'Conectado', 'admin.keyValid': 'Conectado',
'admin.keyInvalid': 'Inválida', 'admin.keyInvalid': 'Inválida',
'admin.keySaved': 'Chaves de API salvas', 'admin.keySaved': 'Chaves de API salvas',
'admin.oidcTitle': 'Login Único (OIDC)', 'admin.oidcTitle': 'Login Único (OIDC)',
'admin.oidcSubtitle': 'admin.oidcSubtitle': 'Permitir login via provedores externos como Google, Apple, Authentik ou Keycloak.',
'Permitir login via provedores externos como Google, Apple, Authentik ou Keycloak.',
'admin.oidcDisplayName': 'Nome exibido', 'admin.oidcDisplayName': 'Nome exibido',
'admin.oidcIssuer': 'URL do emissor', 'admin.oidcIssuer': 'URL do emissor',
'admin.oidcIssuerHint': 'admin.oidcIssuerHint': 'URL do emissor OpenID Connect do provedor, ex.: https://accounts.google.com',
'URL do emissor OpenID Connect do provedor, ex.: https://accounts.google.com',
'admin.oidcSaved': 'Configuração OIDC salva', 'admin.oidcSaved': 'Configuração OIDC salva',
'admin.oidcOnlyMode': 'Desativar login por senha', 'admin.oidcOnlyMode': 'Desativar login por senha',
'admin.oidcOnlyModeHint': 'admin.oidcOnlyModeHint': 'Quando ativado, só é permitido login SSO. Login e cadastro por senha ficam bloqueados.',
'Quando ativado, só é permitido login SSO. Login e cadastro por senha ficam bloqueados.',
'admin.fileTypes': 'Tipos de arquivo permitidos', 'admin.fileTypes': 'Tipos de arquivo permitidos',
'admin.fileTypesHint': 'admin.fileTypesHint': 'Configure quais tipos de arquivo os usuários podem enviar.',
'Configure quais tipos de arquivo os usuários podem enviar.', 'admin.fileTypesFormat': 'Extensões separadas por vírgula (ex.: jpg,png,pdf,doc). Use * para permitir todos.',
'admin.fileTypesFormat':
'Extensões separadas por vírgula (ex.: jpg,png,pdf,doc). Use * para permitir todos.',
'admin.fileTypesSaved': 'Configurações de tipos de arquivo salvas', 'admin.fileTypesSaved': 'Configurações de tipos de arquivo salvas',
'admin.placesPhotos.title': 'Fotos de Locais', 'admin.placesPhotos.title': 'Fotos de Locais',
'admin.placesPhotos.subtitle': 'admin.placesPhotos.subtitle':
@@ -136,8 +120,7 @@ const admin: TranslationStrings = {
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Busca informações detalhadas do local (horários, avaliação, site) da Google Places API. Desative para economizar cota da API.', 'Busca informações detalhadas do local (horários, avaliação, site) da Google Places API. Desative para economizar cota da API.',
'admin.bagTracking.title': 'Rastreamento de malas', 'admin.bagTracking.title': 'Rastreamento de malas',
'admin.bagTracking.subtitle': 'admin.bagTracking.subtitle': 'Ativar peso e atribuição de mala para itens da lista',
'Ativar peso e atribuição de mala para itens da lista',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
'admin.collab.chat.subtitle': 'Mensagens em tempo real para colaboração', 'admin.collab.chat.subtitle': 'Mensagens em tempo real para colaboração',
'admin.collab.notes.title': 'Notas', 'admin.collab.notes.title': 'Notas',
@@ -145,8 +128,7 @@ const admin: TranslationStrings = {
'admin.collab.polls.title': 'Enquetes', 'admin.collab.polls.title': 'Enquetes',
'admin.collab.polls.subtitle': 'Enquetes e votações em grupo', 'admin.collab.polls.subtitle': 'Enquetes e votações em grupo',
'admin.collab.whatsnext.title': 'Próximos passos', 'admin.collab.whatsnext.title': 'Próximos passos',
'admin.collab.whatsnext.subtitle': 'admin.collab.whatsnext.subtitle': 'Sugestões de atividades e próximos passos',
'Sugestões de atividades e próximos passos',
'admin.tabs.config': 'Personalização', 'admin.tabs.config': 'Personalização',
'admin.tabs.defaults': 'Padrões do usuário', 'admin.tabs.defaults': 'Padrões do usuário',
'admin.defaultSettings.title': 'Configurações padrão do usuário', 'admin.defaultSettings.title': 'Configurações padrão do usuário',
@@ -157,8 +139,7 @@ const admin: TranslationStrings = {
'admin.defaultSettings.resetToBuiltIn': 'redefinir', 'admin.defaultSettings.resetToBuiltIn': 'redefinir',
'admin.tabs.templates': 'Modelos de mala', 'admin.tabs.templates': 'Modelos de mala',
'admin.packingTemplates.title': 'Modelos de mala', 'admin.packingTemplates.title': 'Modelos de mala',
'admin.packingTemplates.subtitle': 'admin.packingTemplates.subtitle': 'Crie listas de mala reutilizáveis para suas viagens',
'Crie listas de mala reutilizáveis para suas viagens',
'admin.packingTemplates.create': 'Novo modelo', 'admin.packingTemplates.create': 'Novo modelo',
'admin.packingTemplates.namePlaceholder': 'Nome do modelo (ex.: Praia)', 'admin.packingTemplates.namePlaceholder': 'Nome do modelo (ex.: Praia)',
'admin.packingTemplates.empty': 'Nenhum modelo criado ainda', 'admin.packingTemplates.empty': 'Nenhum modelo criado ainda',
@@ -176,34 +157,24 @@ const admin: TranslationStrings = {
'admin.packingTemplates.saveError': 'Falha ao salvar', 'admin.packingTemplates.saveError': 'Falha ao salvar',
'admin.tabs.addons': 'Complementos', 'admin.tabs.addons': 'Complementos',
'admin.addons.title': 'Complementos', 'admin.addons.title': 'Complementos',
'admin.addons.subtitle': 'admin.addons.subtitle': 'Ative ou desative recursos para personalizar sua experiência no TREK.',
'Ative ou desative recursos para personalizar sua experiência no TREK.',
'admin.addons.catalog.memories.name': 'Memórias', 'admin.addons.catalog.memories.name': 'Memórias',
'admin.addons.catalog.memories.description': 'admin.addons.catalog.memories.description': 'Álbuns de fotos compartilhados em cada viagem',
'Álbuns de fotos compartilhados em cada viagem',
'admin.addons.catalog.packing.name': 'Listas', 'admin.addons.catalog.packing.name': 'Listas',
'admin.addons.catalog.packing.description': 'admin.addons.catalog.packing.description': 'Listas de bagagem e tarefas a fazer para suas viagens',
'Listas de bagagem e tarefas a fazer para suas viagens',
'admin.addons.catalog.budget.name': 'Orçamento', 'admin.addons.catalog.budget.name': 'Orçamento',
'admin.addons.catalog.budget.description': 'admin.addons.catalog.budget.description': 'Acompanhe despesas e planeje o orçamento da viagem',
'Acompanhe despesas e planeje o orçamento da viagem',
'admin.addons.catalog.documents.name': 'Documentos', 'admin.addons.catalog.documents.name': 'Documentos',
'admin.addons.catalog.documents.description': 'admin.addons.catalog.documents.description': 'Armazene e gerencie documentos de viagem',
'Armazene e gerencie documentos de viagem',
'admin.addons.catalog.vacay.name': 'Férias', 'admin.addons.catalog.vacay.name': 'Férias',
'admin.addons.catalog.vacay.description': 'admin.addons.catalog.vacay.description': 'Planejador de férias pessoal com visão em calendário',
'Planejador de férias pessoal com visão em calendário',
'admin.addons.catalog.atlas.name': 'Atlas', 'admin.addons.catalog.atlas.name': 'Atlas',
'admin.addons.catalog.atlas.description': 'admin.addons.catalog.atlas.description': 'Mapa mundial com países visitados e estatísticas',
'Mapa mundial com países visitados e estatísticas',
'admin.addons.catalog.collab.name': 'Colab', 'admin.addons.catalog.collab.name': 'Colab',
'admin.addons.catalog.collab.description': 'admin.addons.catalog.collab.description': 'Notas, enquetes e chat em tempo real para planejar a viagem',
'Notas, enquetes e chat em tempo real para planejar a viagem',
'admin.addons.catalog.mcp.name': 'MCP', 'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'admin.addons.catalog.mcp.description': 'Model Context Protocol para integração com assistentes de IA',
'Model Context Protocol para integração com assistentes de IA', 'admin.addons.subtitleBefore': 'Ative ou desative recursos para personalizar sua ',
'admin.addons.subtitleBefore':
'Ative ou desative recursos para personalizar sua ',
'admin.addons.subtitleAfter': ' experiência.', 'admin.addons.subtitleAfter': ' experiência.',
'admin.addons.enabled': 'Ativado', 'admin.addons.enabled': 'Ativado',
'admin.addons.disabled': 'Desativado', 'admin.addons.disabled': 'Desativado',
@@ -211,13 +182,11 @@ const admin: TranslationStrings = {
'admin.addons.type.global': 'Global', 'admin.addons.type.global': 'Global',
'admin.addons.type.integration': 'Integração', 'admin.addons.type.integration': 'Integração',
'admin.addons.tripHint': 'Disponível como aba em cada viagem', 'admin.addons.tripHint': 'Disponível como aba em cada viagem',
'admin.addons.globalHint': 'admin.addons.globalHint': 'Disponível como seção própria na navegação principal',
'Disponível como seção própria na navegação principal',
'admin.addons.toast.updated': 'Complemento atualizado', 'admin.addons.toast.updated': 'Complemento atualizado',
'admin.addons.toast.error': 'Falha ao atualizar complemento', 'admin.addons.toast.error': 'Falha ao atualizar complemento',
'admin.addons.noAddons': 'Nenhum complemento disponível', 'admin.addons.noAddons': 'Nenhum complemento disponível',
'admin.addons.integrationHint': 'admin.addons.integrationHint': 'Serviços de backend e integrações de API sem página dedicada',
'Serviços de backend e integrações de API sem página dedicada',
'admin.weather.title': 'Dados meteorológicos', 'admin.weather.title': 'Dados meteorológicos',
'admin.weather.badge': 'Desde 24 de março de 2026', 'admin.weather.badge': 'Desde 24 de março de 2026',
'admin.weather.description': 'admin.weather.description':
@@ -225,15 +194,13 @@ const admin: TranslationStrings = {
'admin.weather.forecast': 'Previsão de 16 dias', 'admin.weather.forecast': 'Previsão de 16 dias',
'admin.weather.forecastDesc': 'Antes eram 5 dias (OpenWeatherMap)', 'admin.weather.forecastDesc': 'Antes eram 5 dias (OpenWeatherMap)',
'admin.weather.climate': 'Dados climáticos históricos', 'admin.weather.climate': 'Dados climáticos históricos',
'admin.weather.climateDesc': 'admin.weather.climateDesc': 'Médias dos últimos 85 anos para dias além da previsão de 16 dias',
'Médias dos últimos 85 anos para dias além da previsão de 16 dias',
'admin.weather.requests': '10.000 requisições / dia', 'admin.weather.requests': '10.000 requisições / dia',
'admin.weather.requestsDesc': 'Grátis, sem chave de API', 'admin.weather.requestsDesc': 'Grátis, sem chave de API',
'admin.weather.locationHint': 'admin.weather.locationHint':
'O clima usa o primeiro lugar com coordenadas de cada dia. Se nenhum lugar estiver atribuído ao dia, qualquer lugar da lista serve como referência.', 'O clima usa o primeiro lugar com coordenadas de cada dia. Se nenhum lugar estiver atribuído ao dia, qualquer lugar da lista serve como referência.',
'admin.tabs.audit': 'Auditoria', 'admin.tabs.audit': 'Auditoria',
'admin.audit.subtitle': 'admin.audit.subtitle': 'Eventos sensíveis de segurança e administração (backups, usuários, 2FA, configurações).',
'Eventos sensíveis de segurança e administração (backups, usuários, 2FA, configurações).',
'admin.audit.empty': 'Nenhum registro de auditoria.', 'admin.audit.empty': 'Nenhum registro de auditoria.',
'admin.audit.refresh': 'Atualizar', 'admin.audit.refresh': 'Atualizar',
'admin.audit.loadMore': 'Carregar mais', 'admin.audit.loadMore': 'Carregar mais',
@@ -257,8 +224,7 @@ const admin: TranslationStrings = {
'admin.github.by': 'por', 'admin.github.by': 'por',
'admin.github.support': 'Ajuda a continuar desenvolvendo o TREK', 'admin.github.support': 'Ajuda a continuar desenvolvendo o TREK',
'admin.update.available': 'Atualização disponível', 'admin.update.available': 'Atualização disponível',
'admin.update.text': 'admin.update.text': 'O TREK {version} está disponível. Você está na {current}.',
'O TREK {version} está disponível. Você está na {current}.',
'admin.update.button': 'Ver no GitHub', 'admin.update.button': 'Ver no GitHub',
'admin.update.install': 'Instalar atualização', 'admin.update.install': 'Instalar atualização',
'admin.update.confirmTitle': 'Instalar atualização?', 'admin.update.confirmTitle': 'Instalar atualização?',
@@ -266,8 +232,7 @@ const admin: TranslationStrings = {
'O TREK será atualizado de {current} para {version}. O servidor reiniciará automaticamente em seguida.', 'O TREK será atualizado de {current} para {version}. O servidor reiniciará automaticamente em seguida.',
'admin.update.dataInfo': 'admin.update.dataInfo':
'Todos os seus dados (viagens, usuários, chaves de API, envios, Vacay, Atlas, orçamentos) serão preservados.', 'Todos os seus dados (viagens, usuários, chaves de API, envios, Vacay, Atlas, orçamentos) serão preservados.',
'admin.update.warning': 'admin.update.warning': 'O app ficará brevemente indisponível durante o reinício.',
'O app ficará brevemente indisponível durante o reinício.',
'admin.update.confirm': 'Atualizar agora', 'admin.update.confirm': 'Atualizar agora',
'admin.update.installing': 'Atualizando…', 'admin.update.installing': 'Atualizando…',
'admin.update.success': 'Atualização instalada! O servidor está reiniciando…', 'admin.update.success': 'Atualização instalada! O servidor está reiniciando…',
@@ -275,14 +240,12 @@ const admin: TranslationStrings = {
'admin.update.backupHint': 'Recomendamos criar um backup antes de atualizar.', 'admin.update.backupHint': 'Recomendamos criar um backup antes de atualizar.',
'admin.update.backupLink': 'Ir para Backup', 'admin.update.backupLink': 'Ir para Backup',
'admin.update.howTo': 'Como atualizar', 'admin.update.howTo': 'Como atualizar',
'admin.update.dockerText': 'admin.update.dockerText': 'Sua instância TREK roda no Docker. Para atualizar para {version}, execute no servidor:',
'Sua instância TREK roda no Docker. Para atualizar para {version}, execute no servidor:',
'admin.update.reloadHint': 'Recarregue a página em alguns segundos.', 'admin.update.reloadHint': 'Recarregue a página em alguns segundos.',
'admin.tabs.permissions': 'Permissões', 'admin.tabs.permissions': 'Permissões',
'admin.tabs.mcpTokens': 'Acesso MCP', 'admin.tabs.mcpTokens': 'Acesso MCP',
'admin.mcpTokens.title': 'Acesso MCP', 'admin.mcpTokens.title': 'Acesso MCP',
'admin.mcpTokens.subtitle': 'admin.mcpTokens.subtitle': 'Gerenciar sessões OAuth e tokens de API de todos os usuários',
'Gerenciar sessões OAuth e tokens de API de todos os usuários',
'admin.mcpTokens.sectionTitle': 'Tokens de API', 'admin.mcpTokens.sectionTitle': 'Tokens de API',
'admin.mcpTokens.owner': 'Proprietário', 'admin.mcpTokens.owner': 'Proprietário',
'admin.mcpTokens.tokenName': 'Nome do Token', 'admin.mcpTokens.tokenName': 'Nome do Token',
@@ -303,8 +266,7 @@ const admin: TranslationStrings = {
'admin.oauthSessions.created': 'Criado', 'admin.oauthSessions.created': 'Criado',
'admin.oauthSessions.empty': 'Nenhuma sessão OAuth ativa', 'admin.oauthSessions.empty': 'Nenhuma sessão OAuth ativa',
'admin.oauthSessions.revokeTitle': 'Revogar sessão', 'admin.oauthSessions.revokeTitle': 'Revogar sessão',
'admin.oauthSessions.revokeMessage': 'admin.oauthSessions.revokeMessage': 'Esta sessão OAuth será revogada imediatamente. O cliente perderá o acesso MCP.',
'Esta sessão OAuth será revogada imediatamente. O cliente perderá o acesso MCP.',
'admin.oauthSessions.revokeSuccess': 'Sessão revogada', 'admin.oauthSessions.revokeSuccess': 'Sessão revogada',
'admin.oauthSessions.revokeError': 'Falha ao revogar sessão', 'admin.oauthSessions.revokeError': 'Falha ao revogar sessão',
'admin.oauthSessions.loadError': 'Falha ao carregar sessões OAuth', 'admin.oauthSessions.loadError': 'Falha ao carregar sessões OAuth',
@@ -316,12 +278,9 @@ const admin: TranslationStrings = {
'admin.notifications.adminWebhookPanel.title': 'Webhook de admin', 'admin.notifications.adminWebhookPanel.title': 'Webhook de admin',
'admin.notifications.adminWebhookPanel.hint': 'admin.notifications.adminWebhookPanel.hint':
'Este webhook é usado exclusivamente para notificações de admin (ex. alertas de versão). É independente dos webhooks de usuários e dispara automaticamente quando uma URL está configurada.', 'Este webhook é usado exclusivamente para notificações de admin (ex. alertas de versão). É independente dos webhooks de usuários e dispara automaticamente quando uma URL está configurada.',
'admin.notifications.adminWebhookPanel.saved': 'admin.notifications.adminWebhookPanel.saved': 'URL do webhook de admin salva',
'URL do webhook de admin salva', 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de teste enviado com sucesso',
'admin.notifications.adminWebhookPanel.testSuccess': 'admin.notifications.adminWebhookPanel.testFailed': 'Falha no webhook de teste',
'Webhook de teste enviado com sucesso',
'admin.notifications.adminWebhookPanel.testFailed':
'Falha no webhook de teste',
'admin.notifications.adminWebhookPanel.alwaysOnHint': 'admin.notifications.adminWebhookPanel.alwaysOnHint':
'O webhook de admin dispara automaticamente quando uma URL está configurada', 'O webhook de admin dispara automaticamente quando uma URL está configurada',
'admin.notifications.ntfy': 'Ntfy', 'admin.notifications.ntfy': 'Ntfy',
@@ -340,32 +299,25 @@ const admin: TranslationStrings = {
'admin.notifications.adminNtfyPanel.topicLabel': 'Tópico de admin', 'admin.notifications.adminNtfyPanel.topicLabel': 'Tópico de admin',
'admin.notifications.adminNtfyPanel.topicPlaceholder': 'trek-admin-alerts', 'admin.notifications.adminNtfyPanel.topicPlaceholder': 'trek-admin-alerts',
'admin.notifications.adminNtfyPanel.tokenLabel': 'Token de acesso (opcional)', 'admin.notifications.adminNtfyPanel.tokenLabel': 'Token de acesso (opcional)',
'admin.notifications.adminNtfyPanel.tokenCleared': 'admin.notifications.adminNtfyPanel.tokenCleared': 'Token de acesso admin removido',
'Token de acesso admin removido', 'admin.notifications.adminNtfyPanel.saved': 'Configurações de Ntfy de admin salvas',
'admin.notifications.adminNtfyPanel.saved':
'Configurações de Ntfy de admin salvas',
'admin.notifications.adminNtfyPanel.test': 'Enviar Ntfy de teste', 'admin.notifications.adminNtfyPanel.test': 'Enviar Ntfy de teste',
'admin.notifications.adminNtfyPanel.testSuccess': 'admin.notifications.adminNtfyPanel.testSuccess': 'Ntfy de teste enviado com sucesso',
'Ntfy de teste enviado com sucesso', 'admin.notifications.adminNtfyPanel.testFailed': 'Falha ao enviar Ntfy de teste',
'admin.notifications.adminNtfyPanel.testFailed': 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'O Ntfy de admin sempre dispara quando um tópico está configurado',
'Falha ao enviar Ntfy de teste',
'admin.notifications.adminNtfyPanel.alwaysOnHint':
'O Ntfy de admin sempre dispara quando um tópico está configurado',
'admin.notifications.adminNotificationsHint': 'admin.notifications.adminNotificationsHint':
'Configure quais canais entregam notificações de admin (ex. alertas de versão). O webhook dispara automaticamente se uma URL de webhook de admin estiver definida.', 'Configure quais canais entregam notificações de admin (ex. alertas de versão). O webhook dispara automaticamente se uma URL de webhook de admin estiver definida.',
'admin.notifications.tripReminders.title': 'Lembretes de viagem', 'admin.notifications.tripReminders.title': 'Lembretes de viagem',
'admin.notifications.tripReminders.hint': 'admin.notifications.tripReminders.hint':
'Envia uma notificação de lembrete antes do início de uma viagem (requer dias de lembrete definidos na viagem).', 'Envia uma notificação de lembrete antes do início de uma viagem (requer dias de lembrete definidos na viagem).',
'admin.notifications.tripReminders.enabled': 'Lembretes de viagem ativados', 'admin.notifications.tripReminders.enabled': 'Lembretes de viagem ativados',
'admin.notifications.tripReminders.disabled': 'admin.notifications.tripReminders.disabled': 'Lembretes de viagem desativados',
'Lembretes de viagem desativados',
'admin.tabs.notifications': 'Notificações', 'admin.tabs.notifications': 'Notificações',
'admin.addons.catalog.journey.name': 'Jornada', 'admin.addons.catalog.journey.name': 'Jornada',
'admin.addons.catalog.journey.description': 'admin.addons.catalog.journey.description':
'Rastreamento de viagens e diário de viajante com check-ins, fotos e histórias diárias', 'Rastreamento de viagens e diário de viajante com check-ins, fotos e histórias diárias',
'admin.passkey.title': 'Login com passkey', 'admin.passkey.title': 'Login com passkey',
'admin.passkey.cardHint': 'admin.passkey.cardHint': 'Permite que os usuários entrem com passkeys (WebAuthn). Desativado por padrão.',
'Permite que os usuários entrem com passkeys (WebAuthn). Desativado por padrão.',
'admin.passkey.login': 'Ativar login com passkey', 'admin.passkey.login': 'Ativar login com passkey',
'admin.passkey.loginHint': 'admin.passkey.loginHint':
'Mostra a opção "Entrar com uma passkey" e permite que os usuários cadastrem passkeys nas configurações.', 'Mostra a opção "Entrar com uma passkey" e permite que os usuários cadastrem passkeys nas configurações.',
@@ -383,11 +335,13 @@ const admin: TranslationStrings = {
'admin.passkey.resetConfirm': 'Remover todas as passkeys de {name}?', 'admin.passkey.resetConfirm': 'Remover todas as passkeys de {name}?',
'admin.passkey.resetDone': 'Removida(s) {count} passkey(s)', 'admin.passkey.resetDone': 'Removida(s) {count} passkey(s)',
'admin.defaultSettings.mapProvider': 'Motor de mapas', 'admin.defaultSettings.mapProvider': 'Motor de mapas',
'admin.defaultSettings.mapProviderHint': 'O mapa padrão para todos nesta instância. Cada usuário ainda pode substituí-lo nas próprias configurações.', 'admin.defaultSettings.mapProviderHint':
'O mapa padrão para todos nesta instância. Cada usuário ainda pode substituí-lo nas próprias configurações.',
'admin.defaultSettings.providerLeaflet': 'Padrão (gratuito)', 'admin.defaultSettings.providerLeaflet': 'Padrão (gratuito)',
'admin.defaultSettings.providerMapbox': 'Mapbox (3D)', 'admin.defaultSettings.providerMapbox': 'Mapbox (3D)',
'admin.defaultSettings.mapboxToken': 'Token compartilhado do Mapbox', 'admin.defaultSettings.mapboxToken': 'Token compartilhado do Mapbox',
'admin.defaultSettings.mapboxTokenHint': 'Usado para todos os usuários que não inseriram o próprio token — assim toda a instância usa o Mapbox sem compartilhar a chave individualmente. Armazenado de forma criptografada.', 'admin.defaultSettings.mapboxTokenHint':
'Usado para todos os usuários que não inseriram o próprio token — assim toda a instância usa o Mapbox sem compartilhar a chave individualmente. Armazenado de forma criptografada.',
'admin.defaultSettings.mapboxStyle': 'Estilo do mapa', 'admin.defaultSettings.mapboxStyle': 'Estilo do mapa',
'admin.defaultSettings.mapboxStylePlaceholder': 'Escolha um estilo…', 'admin.defaultSettings.mapboxStylePlaceholder': 'Escolha um estilo…',
'admin.defaultSettings.mapbox3d': 'Edifícios & relevo em 3D', 'admin.defaultSettings.mapbox3d': 'Edifícios & relevo em 3D',
+1 -2
View File
@@ -30,8 +30,7 @@ const atlas: TranslationStrings = {
'atlas.visitedCountries': 'Países visitados', 'atlas.visitedCountries': 'Países visitados',
'atlas.cities': 'Cidades', 'atlas.cities': 'Cidades',
'atlas.noData': 'Ainda sem dados de viagem', 'atlas.noData': 'Ainda sem dados de viagem',
'atlas.noDataHint': 'atlas.noDataHint': 'Crie uma viagem e adicione lugares para ver o mapa mundial',
'Crie uma viagem e adicione lugares para ver o mapa mundial',
'atlas.lastTrip': 'Última viagem', 'atlas.lastTrip': 'Última viagem',
'atlas.nextTrip': 'Próxima viagem', 'atlas.nextTrip': 'Próxima viagem',
'atlas.daysLeft': 'dias restantes', 'atlas.daysLeft': 'dias restantes',
+6 -12
View File
@@ -12,10 +12,8 @@ const backup: TranslationStrings = {
'backup.createFirst': 'Criar primeiro backup', 'backup.createFirst': 'Criar primeiro backup',
'backup.download': 'Baixar', 'backup.download': 'Baixar',
'backup.restore': 'Restaurar', 'backup.restore': 'Restaurar',
'backup.confirm.restore': 'backup.confirm.restore': 'Restaurar o backup "{name}"?\n\nTodos os dados atuais serão substituídos pelo backup.',
'Restaurar o backup "{name}"?\n\nTodos os dados atuais serão substituídos pelo backup.', 'backup.confirm.uploadRestore': 'Enviar e restaurar o arquivo "{name}"?\n\nTodos os dados atuais serão sobrescritos.',
'backup.confirm.uploadRestore':
'Enviar e restaurar o arquivo "{name}"?\n\nTodos os dados atuais serão sobrescritos.',
'backup.confirm.delete': 'Excluir o backup "{name}"?', 'backup.confirm.delete': 'Excluir o backup "{name}"?',
'backup.toast.loadError': 'Falha ao carregar backups', 'backup.toast.loadError': 'Falha ao carregar backups',
'backup.toast.created': 'Backup criado com sucesso', 'backup.toast.created': 'Backup criado com sucesso',
@@ -31,15 +29,13 @@ const backup: TranslationStrings = {
'backup.auto.title': 'Backup automático', 'backup.auto.title': 'Backup automático',
'backup.auto.subtitle': 'Backup automático em agenda', 'backup.auto.subtitle': 'Backup automático em agenda',
'backup.auto.enable': 'Ativar backup automático', 'backup.auto.enable': 'Ativar backup automático',
'backup.auto.enableHint': 'backup.auto.enableHint': 'Backups serão criados automaticamente conforme a agenda escolhida',
'Backups serão criados automaticamente conforme a agenda escolhida',
'backup.auto.interval': 'Intervalo', 'backup.auto.interval': 'Intervalo',
'backup.auto.hour': 'Executar no horário', 'backup.auto.hour': 'Executar no horário',
'backup.auto.hourHint': 'Horário local do servidor (formato {format})', 'backup.auto.hourHint': 'Horário local do servidor (formato {format})',
'backup.auto.dayOfWeek': 'Dia da semana', 'backup.auto.dayOfWeek': 'Dia da semana',
'backup.auto.dayOfMonth': 'Dia do mês', 'backup.auto.dayOfMonth': 'Dia do mês',
'backup.auto.dayOfMonthHint': 'backup.auto.dayOfMonthHint': 'Limitado a 128 para compatibilidade com todos os meses',
'Limitado a 128 para compatibilidade com todos os meses',
'backup.auto.scheduleSummary': 'Agenda', 'backup.auto.scheduleSummary': 'Agenda',
'backup.auto.summaryDaily': 'Todos os dias às {hour}:00', 'backup.auto.summaryDaily': 'Todos os dias às {hour}:00',
'backup.auto.summaryWeekly': 'Toda {day} às {hour}:00', 'backup.auto.summaryWeekly': 'Toda {day} às {hour}:00',
@@ -48,8 +44,7 @@ const backup: TranslationStrings = {
'backup.auto.envLockedHint': 'backup.auto.envLockedHint':
'O backup automático é configurado via variáveis de ambiente Docker. Para alterar essas configurações, atualize o docker-compose.yml e reinicie o contêiner.', 'O backup automático é configurado via variáveis de ambiente Docker. Para alterar essas configurações, atualize o docker-compose.yml e reinicie o contêiner.',
'backup.auto.copyEnv': 'Copiar variáveis de ambiente Docker', 'backup.auto.copyEnv': 'Copiar variáveis de ambiente Docker',
'backup.auto.envCopied': 'backup.auto.envCopied': 'Variáveis de ambiente Docker copiadas para a área de transferência',
'Variáveis de ambiente Docker copiadas para a área de transferência',
'backup.auto.keepLabel': 'Excluir backups antigos após', 'backup.auto.keepLabel': 'Excluir backups antigos após',
'backup.dow.sunday': 'Dom', 'backup.dow.sunday': 'Dom',
'backup.dow.monday': 'Seg', 'backup.dow.monday': 'Seg',
@@ -71,8 +66,7 @@ const backup: TranslationStrings = {
'backup.restoreConfirmTitle': 'Restaurar backup?', 'backup.restoreConfirmTitle': 'Restaurar backup?',
'backup.restoreWarning': 'backup.restoreWarning':
'Todos os dados atuais (viagens, lugares, usuários, envios) serão permanentemente substituídos pelo backup. Esta ação não pode ser desfeita.', 'Todos os dados atuais (viagens, lugares, usuários, envios) serão permanentemente substituídos pelo backup. Esta ação não pode ser desfeita.',
'backup.restoreTip': 'backup.restoreTip': 'Dica: crie um backup do estado atual antes de restaurar.',
'Dica: crie um backup do estado atual antes de restaurar.',
'backup.restoreConfirm': 'Sim, restaurar', 'backup.restoreConfirm': 'Sim, restaurar',
}; };
export default backup; export default backup;
+81 -76
View File
@@ -4,8 +4,7 @@ const budget: TranslationStrings = {
'budget.title': 'Orçamento', 'budget.title': 'Orçamento',
'budget.exportCsv': 'Exportar CSV', 'budget.exportCsv': 'Exportar CSV',
'budget.emptyTitle': 'Nenhum orçamento criado ainda', 'budget.emptyTitle': 'Nenhum orçamento criado ainda',
'budget.emptyText': 'budget.emptyText': 'Crie categorias e lançamentos para planejar o orçamento da viagem',
'Crie categorias e lançamentos para planejar o orçamento da viagem',
'budget.emptyPlaceholder': 'Nome da categoria...', 'budget.emptyPlaceholder': 'Nome da categoria...',
'budget.createCategory': 'Criar categoria', 'budget.createCategory': 'Criar categoria',
'budget.category': 'Categoria', 'budget.category': 'Categoria',
@@ -27,8 +26,7 @@ const budget: TranslationStrings = {
'budget.byCategory': 'Por categoria', 'budget.byCategory': 'Por categoria',
'budget.editTooltip': 'Clique para editar', 'budget.editTooltip': 'Clique para editar',
'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome por lá', 'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome por lá',
'budget.confirm.deleteCategory': 'budget.confirm.deleteCategory': 'Excluir a categoria "{name}" com {count} lançamento(s)?',
'Excluir a categoria "{name}" com {count} lançamento(s)?',
'budget.deleteCategory': 'Excluir categoria', 'budget.deleteCategory': 'Excluir categoria',
'budget.perPerson': 'Por pessoa', 'budget.perPerson': 'Por pessoa',
'budget.paid': 'Pago', 'budget.paid': 'Pago',
@@ -39,78 +37,85 @@ const budget: TranslationStrings = {
'Clique no avatar de um membro em um item do orçamento para marcá-lo em verde — significa que ele pagou. O acerto mostra quem deve quanto a quem.', 'Clique no avatar de um membro em um item do orçamento para marcá-lo em verde — significa que ele pagou. O acerto mostra quem deve quanto a quem.',
'budget.netBalances': 'Saldos líquidos', 'budget.netBalances': 'Saldos líquidos',
'budget.categoriesLabel': 'categorias', 'budget.categoriesLabel': 'categorias',
"costs.you": "Você", 'costs.you': 'Você',
"costs.youShort": "V", 'costs.youShort': 'V',
"costs.youLower": "você", 'costs.youLower': 'você',
"costs.youOwe": "Você deve", 'costs.youOwe': 'Você deve',
"costs.youOweSub": "Você deve pagar os outros", 'costs.youOweSub': 'Você deve pagar os outros',
"costs.youreOwed": "Devem a você", 'costs.youreOwed': 'Devem a você',
"costs.youreOwedSub": "Os outros devem pagar você", 'costs.youreOwedSub': 'Os outros devem pagar você',
"costs.totalSpend": "Gasto total da viagem", 'costs.totalSpend': 'Gasto total da viagem',
"costs.totalSpendSub": "Entre todos os viajantes", 'costs.totalSpendSub': 'Entre todos os viajantes',
"costs.to": "Para", 'costs.to': 'Para',
"costs.from": "De", 'costs.from': 'De',
"costs.allSettled": "Suas contas estão acertadas", 'costs.allSettled': 'Suas contas estão acertadas',
"costs.nothingOwed": "Ninguém deve nada a você", 'costs.nothingOwed': 'Ninguém deve nada a você',
"costs.yourShare": "Sua parte", 'costs.yourShare': 'Sua parte',
"costs.youPaid": "Você pagou", 'costs.youPaid': 'Você pagou',
"costs.expenses": "Despesas", 'costs.expenses': 'Despesas',
"costs.entries": "{count} lançamentos", 'costs.entries': '{count} lançamentos',
"costs.searchPlaceholder": "Buscar despesas…", 'costs.searchPlaceholder': 'Buscar despesas…',
"costs.filter.all": "Todas", 'costs.filter.all': 'Todas',
"costs.filter.mine": "Pagas por mim", 'costs.filter.mine': 'Pagas por mim',
"costs.filter.owed": "Devem a mim", 'costs.filter.owed': 'Devem a mim',
"costs.addExpense": "Adicionar despesa", 'costs.addExpense': 'Adicionar despesa',
"costs.editExpense": "Editar despesa", 'costs.editExpense': 'Editar despesa',
"costs.noMatch": "Nenhuma despesa corresponde à busca.", 'costs.noMatch': 'Nenhuma despesa corresponde à busca.',
"costs.emptyText": "Nenhuma despesa ainda. Adicione a primeira.", 'costs.emptyText': 'Nenhuma despesa ainda. Adicione a primeira.',
"costs.spent": "{amount} gastos", 'costs.spent': '{amount} gastos',
"costs.noDate": "Sem data", 'costs.noDate': 'Sem data',
"costs.noOnePaid": "Ninguém pagou ainda", 'costs.noOnePaid': 'Ninguém pagou ainda',
"costs.youLent": "você emprestou {amount}", 'costs.youLent': 'você emprestou {amount}',
"costs.youBorrowed": "você pegou emprestado {amount}", 'costs.youBorrowed': 'você pegou emprestado {amount}',
"costs.settleUp": "Acertar contas", 'costs.settleUp': 'Acertar contas',
"costs.history": "Histórico", 'costs.history': 'Histórico',
"costs.everyoneSquare": "Todos quitados", 'costs.everyoneSquare': 'Todos quitados',
"costs.nothingOutstanding": "Nenhum pagamento pendente no momento.", 'costs.nothingOutstanding': 'Nenhum pagamento pendente no momento.',
"costs.pay": "paga", 'costs.pay': 'paga',
"costs.pays": "paga", 'costs.pays': 'paga',
"costs.settle": "Acertar", 'costs.settle': 'Acertar',
"costs.balances": "Saldos", 'costs.balances': 'Saldos',
"costs.byCategory": "Por categoria", 'costs.byCategory': 'Por categoria',
"costs.noCategories": "Nenhuma despesa ainda.", 'costs.noCategories': 'Nenhuma despesa ainda.',
"costs.settleHistory": "Histórico de acertos", 'costs.settleHistory': 'Histórico de acertos',
"costs.noSettlements": "Nenhum pagamento acertado ainda.", 'costs.noSettlements': 'Nenhum pagamento acertado ainda.',
"costs.paymentsSettled": "{count} pagamentos acertados", 'costs.paymentsSettled': '{count} pagamentos acertados',
"costs.paid": "pago", 'costs.paid': 'pago',
"costs.undo": "Desfazer", 'costs.undo': 'Desfazer',
"costs.whatFor": "Para que foi?", 'costs.whatFor': 'Para que foi?',
"costs.namePlaceholder": "ex.: jantar, lembranças, gasolina…", 'costs.namePlaceholder': 'ex.: jantar, lembranças, gasolina…',
"costs.totalAmount": "Valor total", 'costs.totalAmount': 'Valor total',
"costs.currency": "Moeda", 'costs.currency': 'Moeda',
"costs.day": "Dia", 'costs.day': 'Dia',
"costs.rateLabel": "1 {from} em {to}", 'costs.rateLabel': '1 {from} em {to}',
"costs.category": "Categoria", 'costs.category': 'Categoria',
"costs.whoPaid": "Quem pagou?", 'costs.whoPaid': 'Quem pagou?',
"costs.splitBetween": "Dividir igualmente entre", 'costs.splitBetween': 'Dividir igualmente entre',
"costs.pickSomeone": "Escolha pelo menos uma pessoa para dividir.", 'costs.pickSomeone': 'Escolha pelo menos uma pessoa para dividir.',
"costs.splitSummary": "Dividido entre {count} · {amount} cada", 'costs.splitSummary': 'Dividido entre {count} · {amount} cada',
"costs.cat.accommodation": "Hospedagem", 'costs.cat.accommodation': 'Hospedagem',
"costs.cat.food": "Comida e bebida", 'costs.cat.food': 'Comida e bebida',
"costs.cat.groceries": "Mercado", 'costs.cat.groceries': 'Mercado',
"costs.cat.transport": "Transporte", 'costs.cat.transport': 'Transporte',
"costs.cat.flights": "Voos", 'costs.cat.flights': 'Voos',
"costs.cat.activities": "Atividades", 'costs.cat.activities': 'Atividades',
"costs.cat.sightseeing": "Passeios turísticos", 'costs.cat.sightseeing': 'Passeios turísticos',
"costs.cat.shopping": "Compras", 'costs.cat.shopping': 'Compras',
"costs.cat.fees": "Taxas e ingressos", 'costs.cat.fees': 'Taxas e ingressos',
"costs.cat.health": "Saúde", 'costs.cat.health': 'Saúde',
"costs.cat.tips": "Gorjetas", 'costs.cat.tips': 'Gorjetas',
"costs.cat.other": "Outros", 'costs.cat.other': 'Outros',
"costs.daysCount": "{count} dias", 'costs.daysCount': '{count} dias',
"costs.travelers": "{count} viajantes", 'costs.travelers': '{count} viajantes',
"costs.liveRate": "taxa ao vivo", 'costs.liveRate': 'taxa ao vivo',
"costs.settleAll": "Acertar tudo", 'costs.settleAll': 'Acertar tudo',
'costs.payment': 'Pagamento',
'costs.editPayment': 'Editar pagamento',
'costs.addPayment': 'Adicionar pagamento',
'costs.unfinished': 'Pendente',
'costs.unfinishedHint': 'Apenas no total — ainda não acertado',
'costs.tapToInclude': 'Toque para incluir',
'costs.amount': 'Valor',
}; };
export default budget; export default budget;
+1 -2
View File
@@ -13,8 +13,7 @@ const categories: TranslationStrings = {
'categories.defaultName': 'Categoria', 'categories.defaultName': 'Categoria',
'categories.update': 'Atualizar', 'categories.update': 'Atualizar',
'categories.create': 'Criar', 'categories.create': 'Criar',
'categories.confirm.delete': 'categories.confirm.delete': 'Excluir categoria? Os lugares desta categoria não serão excluídos.',
'Excluir categoria? Os lugares desta categoria não serão excluídos.',
'categories.toast.loadError': 'Falha ao carregar categorias', 'categories.toast.loadError': 'Falha ao carregar categorias',
'categories.toast.nameRequired': 'Digite um nome', 'categories.toast.nameRequired': 'Digite um nome',
'categories.toast.updated': 'Categoria atualizada', 'categories.toast.updated': 'Categoria atualizada',
+2 -4
View File
@@ -13,10 +13,8 @@ const collab: TranslationStrings = {
'collab.chat.send': 'Enviar', 'collab.chat.send': 'Enviar',
'collab.chat.placeholder': 'Digite uma mensagem...', 'collab.chat.placeholder': 'Digite uma mensagem...',
'collab.chat.empty': 'Inicie a conversa', 'collab.chat.empty': 'Inicie a conversa',
'collab.chat.emptyHint': 'collab.chat.emptyHint': 'As mensagens são compartilhadas com todos os membros da viagem',
'As mensagens são compartilhadas com todos os membros da viagem', 'collab.chat.emptyDesc': 'Compartilhe ideias, planos e atualizações com o grupo',
'collab.chat.emptyDesc':
'Compartilhe ideias, planos e atualizações com o grupo',
'collab.chat.today': 'Hoje', 'collab.chat.today': 'Hoje',
'collab.chat.yesterday': 'Ontem', 'collab.chat.yesterday': 'Ontem',
'collab.chat.deletedMessage': 'apagou uma mensagem', 'collab.chat.deletedMessage': 'apagou uma mensagem',
+4 -8
View File
@@ -20,8 +20,7 @@ const dashboard: TranslationStrings = {
'dashboard.timezoneCustomTzPlaceholder': 'ex.: America/Sao_Paulo', 'dashboard.timezoneCustomTzPlaceholder': 'ex.: America/Sao_Paulo',
'dashboard.timezoneCustomAdd': 'Adicionar', 'dashboard.timezoneCustomAdd': 'Adicionar',
'dashboard.timezoneCustomErrorEmpty': 'Informe um identificador de fuso', 'dashboard.timezoneCustomErrorEmpty': 'Informe um identificador de fuso',
'dashboard.timezoneCustomErrorInvalid': 'dashboard.timezoneCustomErrorInvalid': 'Fuso inválido. Use o formato Europe/Berlin',
'Fuso inválido. Use o formato Europe/Berlin',
'dashboard.timezoneCustomErrorDuplicate': 'Já adicionado', 'dashboard.timezoneCustomErrorDuplicate': 'Já adicionado',
'dashboard.emptyTitle': 'Nenhuma viagem ainda', 'dashboard.emptyTitle': 'Nenhuma viagem ainda',
'dashboard.emptyText': 'Crie sua primeira viagem e comece a planejar!', 'dashboard.emptyText': 'Crie sua primeira viagem e comece a planejar!',
@@ -55,8 +54,7 @@ const dashboard: TranslationStrings = {
'dashboard.toast.restoreError': 'Não foi possível restaurar', 'dashboard.toast.restoreError': 'Não foi possível restaurar',
'dashboard.toast.copied': 'Viagem copiada!', 'dashboard.toast.copied': 'Viagem copiada!',
'dashboard.toast.copyError': 'Não foi possível copiar a viagem', 'dashboard.toast.copyError': 'Não foi possível copiar a viagem',
'dashboard.confirm.delete': 'dashboard.confirm.delete': 'Excluir a viagem "{title}"? Todos os lugares e planos serão excluídos permanentemente.',
'Excluir a viagem "{title}"? Todos os lugares e planos serão excluídos permanentemente.',
'dashboard.editTrip': 'Editar viagem', 'dashboard.editTrip': 'Editar viagem',
'dashboard.createTrip': 'Criar nova viagem', 'dashboard.createTrip': 'Criar nova viagem',
'dashboard.tripTitle': 'Título', 'dashboard.tripTitle': 'Título',
@@ -66,10 +64,8 @@ const dashboard: TranslationStrings = {
'dashboard.startDate': 'Data de início', 'dashboard.startDate': 'Data de início',
'dashboard.endDate': 'Data de término', 'dashboard.endDate': 'Data de término',
'dashboard.dayCount': 'Número de dias', 'dashboard.dayCount': 'Número de dias',
'dashboard.dayCountHint': 'dashboard.dayCountHint': 'Quantos dias planejar quando nenhuma data de viagem for definida.',
'Quantos dias planejar quando nenhuma data de viagem for definida.', 'dashboard.noDateHint': 'Sem datas — serão criados 7 dias padrão. Você pode alterar depois.',
'dashboard.noDateHint':
'Sem datas — serão criados 7 dias padrão. Você pode alterar depois.',
'dashboard.coverImage': 'Imagem de capa', 'dashboard.coverImage': 'Imagem de capa',
'dashboard.addCoverImage': 'Adicionar capa (ou arrastar e soltar)', 'dashboard.addCoverImage': 'Adicionar capa (ou arrastar e soltar)',
'dashboard.addMembers': 'Companheiros de viagem', 'dashboard.addMembers': 'Companheiros de viagem',
+2 -4
View File
@@ -7,10 +7,8 @@ const day: TranslationStrings = {
'day.sunrise': 'Nascer do sol', 'day.sunrise': 'Nascer do sol',
'day.sunset': 'Pôr do sol', 'day.sunset': 'Pôr do sol',
'day.hourlyForecast': 'Previsão por hora', 'day.hourlyForecast': 'Previsão por hora',
'day.climateHint': 'day.climateHint': 'Médias históricas — previsão real disponível até 16 dias desta data.',
'Médias históricas — previsão real disponível até 16 dias desta data.', 'day.noWeather': 'Sem dados meteorológicos. Adicione um lugar com coordenadas.',
'day.noWeather':
'Sem dados meteorológicos. Adicione um lugar com coordenadas.',
'day.overview': 'Resumo do dia', 'day.overview': 'Resumo do dia',
'day.accommodation': 'Hospedagem', 'day.accommodation': 'Hospedagem',
'day.addAccommodation': 'Adicionar hospedagem', 'day.addAccommodation': 'Adicionar hospedagem',
+6 -12
View File
@@ -17,30 +17,24 @@ const dayplan: TranslationStrings = {
'dayplan.optimize': 'Otimizar', 'dayplan.optimize': 'Otimizar',
'dayplan.optimized': 'Rota otimizada', 'dayplan.optimized': 'Rota otimizada',
'dayplan.routeError': 'Falha ao calcular a rota', 'dayplan.routeError': 'Falha ao calcular a rota',
'dayplan.toast.needTwoPlaces': 'dayplan.toast.needTwoPlaces': 'São necessários pelo menos dois lugares para otimizar a rota',
'São necessários pelo menos dois lugares para otimizar a rota',
'dayplan.toast.routeOptimized': 'Rota otimizada', 'dayplan.toast.routeOptimized': 'Rota otimizada',
'dayplan.toast.routeOptimizedFromHotel': 'dayplan.toast.routeOptimizedFromHotel': 'Rota otimizada a partir da sua hospedagem',
'Rota otimizada a partir da sua hospedagem', 'dayplan.toast.noGeoPlaces': 'Nenhum lugar com coordenadas para calcular a rota',
'dayplan.toast.noGeoPlaces':
'Nenhum lugar com coordenadas para calcular a rota',
'dayplan.confirmed': 'Confirmada', 'dayplan.confirmed': 'Confirmada',
'dayplan.pendingRes': 'Pendente', 'dayplan.pendingRes': 'Pendente',
'dayplan.pdf': 'PDF', 'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exportar plano do dia em PDF', 'dayplan.pdfTooltip': 'Exportar plano do dia em PDF',
'dayplan.pdfError': 'Falha ao exportar PDF', 'dayplan.pdfError': 'Falha ao exportar PDF',
'dayplan.cannotReorderTransport': 'dayplan.cannotReorderTransport': 'Reservas com horário fixo não podem ser reordenadas',
'Reservas com horário fixo não podem ser reordenadas',
'dayplan.confirmRemoveTimeTitle': 'Remover horário?', 'dayplan.confirmRemoveTimeTitle': 'Remover horário?',
'dayplan.confirmRemoveTimeBody': 'dayplan.confirmRemoveTimeBody':
'Este lugar tem um horário fixo ({time}). Movê-lo removerá o horário e permitirá ordenação livre.', 'Este lugar tem um horário fixo ({time}). Movê-lo removerá o horário e permitirá ordenação livre.',
'dayplan.confirmRemoveTimeAction': 'Remover horário e mover', 'dayplan.confirmRemoveTimeAction': 'Remover horário e mover',
'dayplan.confirmDeleteNoteTitle': 'Excluir nota?', 'dayplan.confirmDeleteNoteTitle': 'Excluir nota?',
'dayplan.confirmDeleteNoteBody': 'Esta nota será excluída permanentemente.', 'dayplan.confirmDeleteNoteBody': 'Esta nota será excluída permanentemente.',
'dayplan.cannotDropOnTimed': 'dayplan.cannotDropOnTimed': 'Itens não podem ser colocados entre entradas com horário fixo',
'Itens não podem ser colocados entre entradas com horário fixo', 'dayplan.cannotBreakChronology': 'Isso quebraria a ordem cronológica dos itens e reservas agendados',
'dayplan.cannotBreakChronology':
'Isso quebraria a ordem cronológica dos itens e reservas agendados',
'dayplan.mobile.addPlace': 'Adicionar lugar', 'dayplan.mobile.addPlace': 'Adicionar lugar',
'dayplan.mobile.searchPlaces': 'Buscar lugares...', 'dayplan.mobile.searchPlaces': 'Buscar lugares...',
'dayplan.mobile.allAssigned': 'Todos os lugares atribuídos', 'dayplan.mobile.allAssigned': 'Todos os lugares atribuídos',
+1 -2
View File
@@ -55,8 +55,7 @@ const br: NotificationLocale = {
body: 'Recebemos um pedido para redefinir a senha da sua conta TREK. Clique no botão abaixo para definir uma nova senha.', body: 'Recebemos um pedido para redefinir a senha da sua conta TREK. Clique no botão abaixo para definir uma nova senha.',
ctaIntro: 'Redefinir senha', ctaIntro: 'Redefinir senha',
expiry: 'Este link expira em 60 minutos.', expiry: 'Este link expira em 60 minutos.',
ignore: ignore: 'Se você não solicitou isto, pode ignorar este e-mail — sua senha não será alterada.',
'Se você não solicitou isto, pode ignorar este e-mail — sua senha não será alterada.',
}, },
}; };
+4 -8
View File
@@ -13,8 +13,7 @@ const files: TranslationStrings = {
'files.uploadError': 'Falha no envio', 'files.uploadError': 'Falha no envio',
'files.dropzone': 'Solte os arquivos aqui', 'files.dropzone': 'Solte os arquivos aqui',
'files.dropzoneHint': 'ou clique para escolher', 'files.dropzoneHint': 'ou clique para escolher',
'files.allowedTypes': 'files.allowedTypes': 'Imagens, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB',
'Imagens, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB',
'files.uploading': 'Enviando...', 'files.uploading': 'Enviando...',
'files.filterAll': 'Todos', 'files.filterAll': 'Todos',
'files.filterPdf': 'PDFs', 'files.filterPdf': 'PDFs',
@@ -32,8 +31,7 @@ const files: TranslationStrings = {
'files.sourceBooking': 'Reserva', 'files.sourceBooking': 'Reserva',
'files.sourceTransport': 'Transporte', 'files.sourceTransport': 'Transporte',
'files.attach': 'Anexar', 'files.attach': 'Anexar',
'files.pasteHint': 'files.pasteHint': 'Você também pode colar imagens da área de transferência (Ctrl+V)',
'Você também pode colar imagens da área de transferência (Ctrl+V)',
'files.trash': 'Lixeira', 'files.trash': 'Lixeira',
'files.trashEmpty': 'A lixeira está vazia', 'files.trashEmpty': 'A lixeira está vazia',
'files.emptyTrash': 'Esvaziar lixeira', 'files.emptyTrash': 'Esvaziar lixeira',
@@ -53,10 +51,8 @@ const files: TranslationStrings = {
'files.toast.assigned': 'Arquivo atribuído', 'files.toast.assigned': 'Arquivo atribuído',
'files.toast.assignError': 'Falha na atribuição', 'files.toast.assignError': 'Falha na atribuição',
'files.toast.restoreError': 'Falha ao restaurar', 'files.toast.restoreError': 'Falha ao restaurar',
'files.confirm.permanentDelete': 'files.confirm.permanentDelete': 'Excluir permanentemente este arquivo? Não é possível desfazer.',
'Excluir permanentemente este arquivo? Não é possível desfazer.', 'files.confirm.emptyTrash': 'Excluir permanentemente todos os arquivos na lixeira? Não é possível desfazer.',
'files.confirm.emptyTrash':
'Excluir permanentemente todos os arquivos na lixeira? Não é possível desfazer.',
'files.noteLabel': 'Nota', 'files.noteLabel': 'Nota',
'files.notePlaceholder': 'Adicione uma nota...', 'files.notePlaceholder': 'Adicione uma nota...',
}; };
+14 -28
View File
@@ -14,14 +14,12 @@ const journey: TranslationStrings = {
'journey.createError': 'Não foi possível criar a jornada', 'journey.createError': 'Não foi possível criar a jornada',
'journey.deleteError': 'Não foi possível excluir a jornada', 'journey.deleteError': 'Não foi possível excluir a jornada',
'journey.deleteConfirmTitle': 'Excluir', 'journey.deleteConfirmTitle': 'Excluir',
'journey.deleteConfirmMessage': 'journey.deleteConfirmMessage': 'Excluir "{title}"? Isso não pode ser desfeito.',
'Excluir "{title}"? Isso não pode ser desfeito.',
'journey.deleteConfirmGeneric': 'Tem certeza de que deseja excluir isso?', 'journey.deleteConfirmGeneric': 'Tem certeza de que deseja excluir isso?',
'journey.notFound': 'Jornada não encontrada', 'journey.notFound': 'Jornada não encontrada',
'journey.photos': 'Fotos', 'journey.photos': 'Fotos',
'journey.timelineEmpty': 'Nenhuma parada ainda', 'journey.timelineEmpty': 'Nenhuma parada ainda',
'journey.timelineEmptyHint': 'journey.timelineEmptyHint': 'Adicione um check-in ou escreva uma entrada no diário para começar',
'Adicione um check-in ou escreva uma entrada no diário para começar',
'journey.status.draft': 'Rascunho', 'journey.status.draft': 'Rascunho',
'journey.status.active': 'Ativa', 'journey.status.active': 'Ativa',
'journey.status.completed': 'Concluída', 'journey.status.completed': 'Concluída',
@@ -47,30 +45,25 @@ const journey: TranslationStrings = {
'journey.editor.titlePlaceholder': 'Dê um nome a este momento...', 'journey.editor.titlePlaceholder': 'Dê um nome a este momento...',
'journey.editor.bodyPlaceholder': 'Conte a história deste dia...', 'journey.editor.bodyPlaceholder': 'Conte a história deste dia...',
'journey.editor.placePlaceholder': 'Localização (opcional)', 'journey.editor.placePlaceholder': 'Localização (opcional)',
'journey.editor.tagsPlaceholder': 'journey.editor.tagsPlaceholder': 'Tags: joia escondida, melhor refeição, preciso voltar...',
'Tags: joia escondida, melhor refeição, preciso voltar...',
'journey.visibility.private': 'Privado', 'journey.visibility.private': 'Privado',
'journey.visibility.shared': 'Compartilhado', 'journey.visibility.shared': 'Compartilhado',
'journey.visibility.public': 'Público', 'journey.visibility.public': 'Público',
'journey.emptyState.title': 'Sua história começa aqui', 'journey.emptyState.title': 'Sua história começa aqui',
'journey.emptyState.subtitle': 'journey.emptyState.subtitle': 'Faça check-in em um lugar ou escreva sua primeira entrada no diário',
'Faça check-in em um lugar ou escreva sua primeira entrada no diário', 'journey.frontpage.subtitle': 'Transforme suas viagens em histórias que você nunca vai esquecer',
'journey.frontpage.subtitle':
'Transforme suas viagens em histórias que você nunca vai esquecer',
'journey.frontpage.createJourney': 'Criar jornada', 'journey.frontpage.createJourney': 'Criar jornada',
'journey.frontpage.activeJourney': 'Jornada ativa', 'journey.frontpage.activeJourney': 'Jornada ativa',
'journey.frontpage.allJourneys': 'Todas as jornadas', 'journey.frontpage.allJourneys': 'Todas as jornadas',
'journey.frontpage.journeys': 'jornadas', 'journey.frontpage.journeys': 'jornadas',
'journey.frontpage.createNew': 'Criar uma nova jornada', 'journey.frontpage.createNew': 'Criar uma nova jornada',
'journey.frontpage.createNewSub': 'journey.frontpage.createNewSub': 'Escolha viagens, escreva histórias, compartilhe suas aventuras',
'Escolha viagens, escreva histórias, compartilhe suas aventuras',
'journey.frontpage.live': 'Ao vivo', 'journey.frontpage.live': 'Ao vivo',
'journey.frontpage.synced': 'Sincronizado', 'journey.frontpage.synced': 'Sincronizado',
'journey.frontpage.continueWriting': 'Continuar escrevendo', 'journey.frontpage.continueWriting': 'Continuar escrevendo',
'journey.frontpage.updated': 'Atualizado {time}', 'journey.frontpage.updated': 'Atualizado {time}',
'journey.frontpage.suggestionLabel': 'A viagem acabou de terminar', 'journey.frontpage.suggestionLabel': 'A viagem acabou de terminar',
'journey.frontpage.suggestionText': 'journey.frontpage.suggestionText': 'Transforme <strong>{title}</strong> em uma jornada',
'Transforme <strong>{title}</strong> em uma jornada',
'journey.frontpage.dismiss': 'Dispensar', 'journey.frontpage.dismiss': 'Dispensar',
'journey.frontpage.journeyName': 'Nome da jornada', 'journey.frontpage.journeyName': 'Nome da jornada',
'journey.frontpage.namePlaceholder': 'ex. Sudeste Asiático 2026', 'journey.frontpage.namePlaceholder': 'ex. Sudeste Asiático 2026',
@@ -85,11 +78,9 @@ const journey: TranslationStrings = {
'journey.detail.newEntry': 'Nova entrada', 'journey.detail.newEntry': 'Nova entrada',
'journey.detail.editEntry': 'Editar entrada', 'journey.detail.editEntry': 'Editar entrada',
'journey.detail.noEntries': 'Nenhuma entrada ainda', 'journey.detail.noEntries': 'Nenhuma entrada ainda',
'journey.detail.noEntriesHint': 'journey.detail.noEntriesHint': 'Adicione uma viagem para começar com entradas preliminares',
'Adicione uma viagem para começar com entradas preliminares',
'journey.detail.noPhotos': 'Nenhuma foto ainda', 'journey.detail.noPhotos': 'Nenhuma foto ainda',
'journey.detail.noPhotosHint': 'journey.detail.noPhotosHint': 'Envie fotos para as entradas ou explore sua biblioteca do Immich/Synology',
'Envie fotos para as entradas ou explore sua biblioteca do Immich/Synology',
'journey.detail.journeyStats': 'Estatísticas da jornada', 'journey.detail.journeyStats': 'Estatísticas da jornada',
'journey.detail.syncedTrips': 'Viagens sincronizadas', 'journey.detail.syncedTrips': 'Viagens sincronizadas',
'journey.detail.noTripsLinked': 'Nenhuma viagem vinculada ainda', 'journey.detail.noTripsLinked': 'Nenhuma viagem vinculada ainda',
@@ -110,14 +101,12 @@ const journey: TranslationStrings = {
'journey.verdict.couldBeBetter': 'Poderia ser melhor', 'journey.verdict.couldBeBetter': 'Poderia ser melhor',
'journey.synced.places': 'lugares', 'journey.synced.places': 'lugares',
'journey.synced.synced': 'sincronizado', 'journey.synced.synced': 'sincronizado',
'journey.editor.discardChangesConfirm': 'journey.editor.discardChangesConfirm': 'Você tem alterações não salvas. Descartá-las?',
'Você tem alterações não salvas. Descartá-las?',
'journey.editor.uploadFailed': 'Falha ao enviar fotos', 'journey.editor.uploadFailed': 'Falha ao enviar fotos',
'journey.editor.uploadPhotos': 'Enviar fotos', 'journey.editor.uploadPhotos': 'Enviar fotos',
'journey.editor.uploading': 'Enviando...', 'journey.editor.uploading': 'Enviando...',
'journey.editor.uploadingProgress': 'Enviando {done}/{total}…', 'journey.editor.uploadingProgress': 'Enviando {done}/{total}…',
'journey.editor.uploadPartialFailed': 'journey.editor.uploadPartialFailed': '{failed} de {total} fotos falharam — salve novamente para tentar',
'{failed} de {total} fotos falharam — salve novamente para tentar',
'journey.editor.fromGallery': 'Da galeria', 'journey.editor.fromGallery': 'Da galeria',
'journey.editor.allPhotosAdded': 'Todas as fotos já foram adicionadas', 'journey.editor.allPhotosAdded': 'Todas as fotos já foram adicionadas',
'journey.editor.writeStory': 'Escreva sua história...', 'journey.editor.writeStory': 'Escreva sua história...',
@@ -196,12 +185,10 @@ const journey: TranslationStrings = {
'journey.settings.reopenJourney': 'Restaurar Jornada', 'journey.settings.reopenJourney': 'Restaurar Jornada',
'journey.settings.archived': 'Jornada arquivada', 'journey.settings.archived': 'Jornada arquivada',
'journey.settings.reopened': 'Jornada reaberta', 'journey.settings.reopened': 'Jornada reaberta',
'journey.settings.endDescription': 'journey.settings.endDescription': 'Oculta o selo Ao Vivo. Você pode reabrir a qualquer momento.',
'Oculta o selo Ao Vivo. Você pode reabrir a qualquer momento.',
'journey.settings.delete': 'Excluir', 'journey.settings.delete': 'Excluir',
'journey.settings.deleteJourney': 'Excluir jornada', 'journey.settings.deleteJourney': 'Excluir jornada',
'journey.settings.deleteMessage': 'journey.settings.deleteMessage': 'Excluir "{title}"? Todas as entradas e fotos serão perdidas.',
'Excluir "{title}"? Todas as entradas e fotos serão perdidas.',
'journey.settings.saved': 'Configurações salvas', 'journey.settings.saved': 'Configurações salvas',
'journey.settings.saveFailed': 'Não foi possível salvar', 'journey.settings.saveFailed': 'Não foi possível salvar',
'journey.settings.coverUpdated': 'Capa atualizada', 'journey.settings.coverUpdated': 'Capa atualizada',
@@ -212,8 +199,7 @@ const journey: TranslationStrings = {
'journey.photosUploadFailed': 'Algumas fotos não foram enviadas', 'journey.photosUploadFailed': 'Algumas fotos não foram enviadas',
'journey.photosAdded': '{count} fotos adicionadas', 'journey.photosAdded': '{count} fotos adicionadas',
'journey.public.notFound': 'Não encontrado', 'journey.public.notFound': 'Não encontrado',
'journey.public.notFoundMessage': 'journey.public.notFoundMessage': 'Esta jornada não existe ou o link expirou.',
'Esta jornada não existe ou o link expirou.',
'journey.public.readOnly': 'Somente leitura · Jornada pública', 'journey.public.readOnly': 'Somente leitura · Jornada pública',
'journey.public.tagline': 'Kit de recursos e exploração de viagens', 'journey.public.tagline': 'Kit de recursos e exploração de viagens',
'journey.public.sharedVia': 'Compartilhado via', 'journey.public.sharedVia': 'Compartilhado via',
+11 -22
View File
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
const login: TranslationStrings = { const login: TranslationStrings = {
'login.error': 'Falha no login. Verifique suas credenciais.', 'login.error': 'Falha no login. Verifique suas credenciais.',
'login.tagline': 'Suas viagens.\nSeu plano.', 'login.tagline': 'Suas viagens.\nSeu plano.',
'login.description': 'login.description': 'Planeje viagens em equipe com mapas interativos, orçamento e sincronização em tempo real.',
'Planeje viagens em equipe com mapas interativos, orçamento e sincronização em tempo real.',
'login.features.maps': 'Mapas interativos', 'login.features.maps': 'Mapas interativos',
'login.features.mapsDesc': 'Google Places, rotas e agrupamento', 'login.features.mapsDesc': 'Google Places, rotas e agrupamento',
'login.features.realtime': 'Sincronização em tempo real', 'login.features.realtime': 'Sincronização em tempo real',
@@ -27,8 +26,7 @@ const login: TranslationStrings = {
'login.signingIn': 'Entrando…', 'login.signingIn': 'Entrando…',
'login.signIn': 'Entrar', 'login.signIn': 'Entrar',
'login.createAdmin': 'Criar conta de administrador', 'login.createAdmin': 'Criar conta de administrador',
'login.createAdminHint': 'login.createAdminHint': 'Configure a primeira conta de administrador do TREK.',
'Configure a primeira conta de administrador do TREK.',
'login.setNewPassword': 'Definir nova senha', 'login.setNewPassword': 'Definir nova senha',
'login.setNewPasswordHint': 'Você deve alterar sua senha antes de continuar.', 'login.setNewPasswordHint': 'Você deve alterar sua senha antes de continuar.',
'login.createAccount': 'Criar conta', 'login.createAccount': 'Criar conta',
@@ -39,16 +37,14 @@ const login: TranslationStrings = {
'login.register': 'Cadastrar', 'login.register': 'Cadastrar',
'login.emailPlaceholder': 'seu@email.com', 'login.emailPlaceholder': 'seu@email.com',
'login.username': 'Nome de usuário', 'login.username': 'Nome de usuário',
'login.oidc.registrationDisabled': 'login.oidc.registrationDisabled': 'Cadastro desativado. Fale com o administrador.',
'Cadastro desativado. Fale com o administrador.',
'login.oidc.noEmail': 'Nenhum e-mail recebido do provedor.', 'login.oidc.noEmail': 'Nenhum e-mail recebido do provedor.',
'login.oidc.tokenFailed': 'Falha na autenticação.', 'login.oidc.tokenFailed': 'Falha na autenticação.',
'login.oidc.invalidState': 'Sessão inválida. Tente novamente.', 'login.oidc.invalidState': 'Sessão inválida. Tente novamente.',
'login.demoFailed': 'Falha no login de demonstração', 'login.demoFailed': 'Falha no login de demonstração',
'login.oidcSignIn': 'Entrar com {name}', 'login.oidcSignIn': 'Entrar com {name}',
'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.', 'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.',
'login.oidcLoggedOut': 'login.oidcLoggedOut': 'Você foi desconectado. Entre novamente usando o provedor SSO.',
'Você foi desconectado. Entre novamente usando o provedor SSO.',
'login.demoHint': 'Experimente a demonstração — sem cadastro', 'login.demoHint': 'Experimente a demonstração — sem cadastro',
'login.mfaTitle': 'Autenticação em duas etapas', 'login.mfaTitle': 'Autenticação em duas etapas',
'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.', 'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.',
@@ -64,8 +60,7 @@ const login: TranslationStrings = {
'login.forgotPassword': 'Esqueceu a senha?', 'login.forgotPassword': 'Esqueceu a senha?',
'login.rememberMe': 'Lembrar de mim', 'login.rememberMe': 'Lembrar de mim',
'login.forgotPasswordTitle': 'Redefinir sua senha', 'login.forgotPasswordTitle': 'Redefinir sua senha',
'login.forgotPasswordBody': 'login.forgotPasswordBody': 'Digite o e-mail cadastrado. Se houver uma conta, enviaremos um link de redefinição.',
'Digite o e-mail cadastrado. Se houver uma conta, enviaremos um link de redefinição.',
'login.forgotPasswordSubmit': 'Enviar link', 'login.forgotPasswordSubmit': 'Enviar link',
'login.forgotPasswordSentTitle': 'Verifique seu e-mail', 'login.forgotPasswordSentTitle': 'Verifique seu e-mail',
'login.forgotPasswordSentBody': 'login.forgotPasswordSentBody':
@@ -78,22 +73,16 @@ const login: TranslationStrings = {
'login.passwordsDontMatch': 'As senhas não coincidem', 'login.passwordsDontMatch': 'As senhas não coincidem',
'login.mfaCode': 'Código 2FA', 'login.mfaCode': 'Código 2FA',
'login.resetPasswordTitle': 'Definir uma nova senha', 'login.resetPasswordTitle': 'Definir uma nova senha',
'login.resetPasswordBody': 'login.resetPasswordBody': 'Escolha uma senha forte que você ainda não tenha usado aqui. Mínimo de 8 caracteres.',
'Escolha uma senha forte que você ainda não tenha usado aqui. Mínimo de 8 caracteres.', 'login.resetPasswordMfaBody': 'Digite seu código 2FA ou um código de backup para concluir a redefinição.',
'login.resetPasswordMfaBody':
'Digite seu código 2FA ou um código de backup para concluir a redefinição.',
'login.resetPasswordSubmit': 'Redefinir senha', 'login.resetPasswordSubmit': 'Redefinir senha',
'login.resetPasswordVerify': 'Verificar e redefinir', 'login.resetPasswordVerify': 'Verificar e redefinir',
'login.resetPasswordSuccessTitle': 'Senha atualizada', 'login.resetPasswordSuccessTitle': 'Senha atualizada',
'login.resetPasswordSuccessBody': 'login.resetPasswordSuccessBody': 'Agora você pode entrar com sua nova senha.',
'Agora você pode entrar com sua nova senha.',
'login.resetPasswordInvalidLink': 'Link de redefinição inválido', 'login.resetPasswordInvalidLink': 'Link de redefinição inválido',
'login.resetPasswordInvalidLinkBody': 'login.resetPasswordInvalidLinkBody': 'Este link está ausente ou corrompido. Solicite um novo para continuar.',
'Este link está ausente ou corrompido. Solicite um novo para continuar.', 'login.resetPasswordFailed': 'Falha na redefinição. O link pode ter expirado.',
'login.resetPasswordFailed':
'Falha na redefinição. O link pode ter expirado.',
'login.passkey.signIn': 'Entrar com uma passkey', 'login.passkey.signIn': 'Entrar com uma passkey',
'login.passkey.failed': 'login.passkey.failed': 'Falha ao entrar com passkey. Tente novamente.',
'Falha ao entrar com passkey. Tente novamente.',
}; };
export default login; export default login;
+8 -16
View File
@@ -3,21 +3,18 @@ import type { TranslationStrings } from '../types';
const memories: TranslationStrings = { const memories: TranslationStrings = {
'memories.title': 'Fotos', 'memories.title': 'Fotos',
'memories.notConnected': 'Immich não conectado', 'memories.notConnected': 'Immich não conectado',
'memories.notConnectedHint': 'memories.notConnectedHint': 'Conecte sua instância Immich nas Configurações para ver suas fotos de viagem aqui.',
'Conecte sua instância Immich nas Configurações para ver suas fotos de viagem aqui.',
'memories.notConnectedMultipleHint': 'memories.notConnectedMultipleHint':
'Conecte um destes provedores de fotos: {provider_names} nas Configurações para poder adicionar fotos a esta viagem.', 'Conecte um destes provedores de fotos: {provider_names} nas Configurações para poder adicionar fotos a esta viagem.',
'memories.noDates': 'Adicione datas à sua viagem para carregar fotos.', 'memories.noDates': 'Adicione datas à sua viagem para carregar fotos.',
'memories.noPhotos': 'Nenhuma foto encontrada', 'memories.noPhotos': 'Nenhuma foto encontrada',
'memories.noPhotosHint': 'memories.noPhotosHint': 'Nenhuma foto encontrada no Immich para o período desta viagem.',
'Nenhuma foto encontrada no Immich para o período desta viagem.',
'memories.photosFound': 'fotos', 'memories.photosFound': 'fotos',
'memories.fromOthers': 'de outros', 'memories.fromOthers': 'de outros',
'memories.sharePhotos': 'Compartilhar fotos', 'memories.sharePhotos': 'Compartilhar fotos',
'memories.sharing': 'Compartilhando', 'memories.sharing': 'Compartilhando',
'memories.reviewTitle': 'Revise suas fotos', 'memories.reviewTitle': 'Revise suas fotos',
'memories.reviewHint': 'memories.reviewHint': 'Clique nas fotos para excluí-las do compartilhamento.',
'Clique nas fotos para excluí-las do compartilhamento.',
'memories.shareCount': 'Compartilhar {count} fotos', 'memories.shareCount': 'Compartilhar {count} fotos',
'memories.providerUrl': 'URL do servidor', 'memories.providerUrl': 'URL do servidor',
'memories.providerApiKey': 'Chave de API', 'memories.providerApiKey': 'Chave de API',
@@ -26,8 +23,7 @@ const memories: TranslationStrings = {
'memories.providerOTP': 'Código MFA (se habilitado)', 'memories.providerOTP': 'Código MFA (se habilitado)',
'memories.skipSSLVerification': 'Pular verificação de certificado SSL', 'memories.skipSSLVerification': 'Pular verificação de certificado SSL',
'memories.immichAutoUpload': 'Espelhar fotos da jornada no Immich ao enviar', 'memories.immichAutoUpload': 'Espelhar fotos da jornada no Immich ao enviar',
'memories.providerUrlHintSynology': 'memories.providerUrlHintSynology': 'Inclua o caminho do aplicativo Photos na URL, ex. https://nas:5001/photo',
'Inclua o caminho do aplicativo Photos na URL, ex. https://nas:5001/photo',
'memories.testConnection': 'Testar conexão', 'memories.testConnection': 'Testar conexão',
'memories.testShort': 'Testar', 'memories.testShort': 'Testar',
'memories.testFirst': 'Teste a conexão primeiro', 'memories.testFirst': 'Teste a conexão primeiro',
@@ -38,8 +34,7 @@ const memories: TranslationStrings = {
'memories.saved': 'Configurações do {provider_name} salvas', 'memories.saved': 'Configurações do {provider_name} salvas',
'memories.providerDisconnectedBanner': 'memories.providerDisconnectedBanner':
'Sua conexão com {provider_name} foi perdida. Reconecte nas Configurações para ver as fotos.', 'Sua conexão com {provider_name} foi perdida. Reconecte nas Configurações para ver as fotos.',
'memories.saveError': 'memories.saveError': 'Não foi possível salvar as configurações de {provider_name}',
'Não foi possível salvar as configurações de {provider_name}',
'memories.addPhotos': 'Adicionar fotos', 'memories.addPhotos': 'Adicionar fotos',
'memories.linkAlbum': 'Vincular álbum', 'memories.linkAlbum': 'Vincular álbum',
'memories.selectAlbum': 'Selecionar álbum do Immich', 'memories.selectAlbum': 'Selecionar álbum do Immich',
@@ -73,11 +68,8 @@ const memories: TranslationStrings = {
'memories.error.addPhotos': 'Falha ao adicionar fotos', 'memories.error.addPhotos': 'Falha ao adicionar fotos',
'memories.error.removePhoto': 'Falha ao remover foto', 'memories.error.removePhoto': 'Falha ao remover foto',
'memories.error.toggleSharing': 'Falha ao atualizar compartilhamento', 'memories.error.toggleSharing': 'Falha ao atualizar compartilhamento',
'memories.saveRouteNotConfigured': 'memories.saveRouteNotConfigured': 'A rota de salvamento não está configurada para este provedor',
'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.testRouteNotConfigured': 'memories.fillRequiredFields': 'Por favor preencha todos os campos obrigatórios',
'A rota de teste não está configurada para este provedor',
'memories.fillRequiredFields':
'Por favor preencha todos os campos obrigatórios',
}; };
export default memories; export default memories;
+2 -4
View File
@@ -14,8 +14,7 @@ const notif: TranslationStrings = {
'notif.todo_due.title': 'Tarefa com vencimento', 'notif.todo_due.title': 'Tarefa com vencimento',
'notif.todo_due.text': '{todo} em {trip} vence em {due}', 'notif.todo_due.text': '{todo} em {trip} vence em {due}',
'notif.vacay_invite.title': 'Convite Vacay Fusion', 'notif.vacay_invite.title': 'Convite Vacay Fusion',
'notif.vacay_invite.text': 'notif.vacay_invite.text': '{actor} convidou você para fundir planos de férias',
'{actor} convidou você para fundir planos de férias',
'notif.photos_shared.title': 'Fotos compartilhadas', 'notif.photos_shared.title': 'Fotos compartilhadas',
'notif.photos_shared.text': '{actor} compartilhou {count} foto(s) em {trip}', 'notif.photos_shared.text': '{actor} compartilhou {count} foto(s) em {trip}',
'notif.collab_message.title': 'Nova mensagem', 'notif.collab_message.title': 'Nova mensagem',
@@ -36,7 +35,6 @@ const notif: TranslationStrings = {
'notif.generic.title': 'Notificação', 'notif.generic.title': 'Notificação',
'notif.generic.text': 'Você tem uma nova notificação', 'notif.generic.text': 'Você tem uma nova notificação',
'notif.dev.unknown_event.title': '[DEV] Evento desconhecido', 'notif.dev.unknown_event.title': '[DEV] Evento desconhecido',
'notif.dev.unknown_event.text': 'notif.dev.unknown_event.text': 'O tipo de evento "{event}" não está registrado em EVENT_NOTIFICATION_CONFIG',
'O tipo de evento "{event}" não está registrado em EVENT_NOTIFICATION_CONFIG',
}; };
export default notif; export default notif;
+1 -2
View File
@@ -26,8 +26,7 @@ const notifications: TranslationStrings = {
'notifications.test.navigateText': 'Notificação de teste de navegação.', 'notifications.test.navigateText': 'Notificação de teste de navegação.',
'notifications.test.goThere': 'Ir lá', 'notifications.test.goThere': 'Ir lá',
'notifications.test.adminTitle': 'Transmissão do admin', 'notifications.test.adminTitle': 'Transmissão do admin',
'notifications.test.adminText': 'notifications.test.adminText': '{actor} enviou uma notificação de teste para todos os admins.',
'{actor} enviou uma notificação de teste para todos os admins.',
'notifications.test.tripTitle': '{actor} postou na sua viagem', 'notifications.test.tripTitle': '{actor} postou na sua viagem',
'notifications.test.tripText': 'Notificação de teste para a viagem "{trip}".', 'notifications.test.tripText': 'Notificação de teste para a viagem "{trip}".',
'notifications.versionAvailable.title': 'Atualização disponível', 'notifications.versionAvailable.title': 'Atualização disponível',
+30 -60
View File
@@ -17,80 +17,55 @@ const oauth: TranslationStrings = {
'oauth.scope.trips:read.label': 'Ver viagens e itinerários', 'oauth.scope.trips:read.label': 'Ver viagens e itinerários',
'oauth.scope.trips:read.description': 'Ler viagens, dias, notas e membros', 'oauth.scope.trips:read.description': 'Ler viagens, dias, notas e membros',
'oauth.scope.trips:write.label': 'Editar viagens e itinerários', 'oauth.scope.trips:write.label': 'Editar viagens e itinerários',
'oauth.scope.trips:write.description': 'oauth.scope.trips:write.description': 'Criar e atualizar viagens, dias, notas e gerenciar membros',
'Criar e atualizar viagens, dias, notas e gerenciar membros',
'oauth.scope.trips:delete.label': 'Excluir viagens', 'oauth.scope.trips:delete.label': 'Excluir viagens',
'oauth.scope.trips:delete.description': 'oauth.scope.trips:delete.description': 'Excluir viagens permanentemente — esta ação é irreversível',
'Excluir viagens permanentemente — esta ação é irreversível',
'oauth.scope.trips:share.label': 'Gerenciar links de compartilhamento', 'oauth.scope.trips:share.label': 'Gerenciar links de compartilhamento',
'oauth.scope.trips:share.description': 'oauth.scope.trips:share.description': 'Criar, atualizar e revogar links de compartilhamento públicos',
'Criar, atualizar e revogar links de compartilhamento públicos',
'oauth.scope.places:read.label': 'Ver locais e dados do mapa', 'oauth.scope.places:read.label': 'Ver locais e dados do mapa',
'oauth.scope.places:read.description': 'oauth.scope.places:read.description': 'Ler locais, atribuições de dias, tags e categorias',
'Ler locais, atribuições de dias, tags e categorias',
'oauth.scope.places:write.label': 'Gerenciar locais', 'oauth.scope.places:write.label': 'Gerenciar locais',
'oauth.scope.places:write.description': 'oauth.scope.places:write.description': 'Criar, atualizar e excluir locais, atribuições e tags',
'Criar, atualizar e excluir locais, atribuições e tags',
'oauth.scope.atlas:read.label': 'Ver Atlas', 'oauth.scope.atlas:read.label': 'Ver Atlas',
'oauth.scope.atlas:read.description': 'oauth.scope.atlas:read.description': 'Ler países visitados, regiões e lista de desejos',
'Ler países visitados, regiões e lista de desejos',
'oauth.scope.atlas:write.label': 'Gerenciar Atlas', 'oauth.scope.atlas:write.label': 'Gerenciar Atlas',
'oauth.scope.atlas:write.description': 'oauth.scope.atlas:write.description': 'Marcar países e regiões como visitados, gerenciar lista de desejos',
'Marcar países e regiões como visitados, gerenciar lista de desejos',
'oauth.scope.packing:read.label': 'Ver listas de bagagem', 'oauth.scope.packing:read.label': 'Ver listas de bagagem',
'oauth.scope.packing:read.description': 'oauth.scope.packing:read.description': 'Ler itens, malas e responsáveis por categoria',
'Ler itens, malas e responsáveis por categoria',
'oauth.scope.packing:write.label': 'Gerenciar listas de bagagem', 'oauth.scope.packing:write.label': 'Gerenciar listas de bagagem',
'oauth.scope.packing:write.description': 'oauth.scope.packing:write.description': 'Adicionar, atualizar, excluir, marcar e reordenar itens e malas',
'Adicionar, atualizar, excluir, marcar e reordenar itens e malas',
'oauth.scope.todos:read.label': 'Ver listas de tarefas', 'oauth.scope.todos:read.label': 'Ver listas de tarefas',
'oauth.scope.todos:read.description': 'oauth.scope.todos:read.description': 'Ler tarefas da viagem e responsáveis por categoria',
'Ler tarefas da viagem e responsáveis por categoria',
'oauth.scope.todos:write.label': 'Gerenciar listas de tarefas', 'oauth.scope.todos:write.label': 'Gerenciar listas de tarefas',
'oauth.scope.todos:write.description': 'oauth.scope.todos:write.description': 'Criar, atualizar, marcar, excluir e reordenar tarefas',
'Criar, atualizar, marcar, excluir e reordenar tarefas',
'oauth.scope.budget:read.label': 'Ver orçamento', 'oauth.scope.budget:read.label': 'Ver orçamento',
'oauth.scope.budget:read.description': 'oauth.scope.budget:read.description': 'Ler itens de orçamento e detalhamento de despesas',
'Ler itens de orçamento e detalhamento de despesas',
'oauth.scope.budget:write.label': 'Gerenciar orçamento', 'oauth.scope.budget:write.label': 'Gerenciar orçamento',
'oauth.scope.budget:write.description': 'oauth.scope.budget:write.description': 'Criar, atualizar e excluir itens de orçamento',
'Criar, atualizar e excluir itens de orçamento',
'oauth.scope.reservations:read.label': 'Ver reservas', 'oauth.scope.reservations:read.label': 'Ver reservas',
'oauth.scope.reservations:read.description': 'oauth.scope.reservations:read.description': 'Ler reservas e detalhes de acomodação',
'Ler reservas e detalhes de acomodação',
'oauth.scope.reservations:write.label': 'Gerenciar reservas', 'oauth.scope.reservations:write.label': 'Gerenciar reservas',
'oauth.scope.reservations:write.description': 'oauth.scope.reservations:write.description': 'Criar, atualizar, excluir e reordenar reservas',
'Criar, atualizar, excluir e reordenar reservas',
'oauth.scope.collab:read.label': 'Ver colaboração', 'oauth.scope.collab:read.label': 'Ver colaboração',
'oauth.scope.collab:read.description': 'oauth.scope.collab:read.description': 'Ler notas colaborativas, enquetes e mensagens',
'Ler notas colaborativas, enquetes e mensagens',
'oauth.scope.collab:write.label': 'Gerenciar colaboração', 'oauth.scope.collab:write.label': 'Gerenciar colaboração',
'oauth.scope.collab:write.description': 'oauth.scope.collab:write.description': 'Criar, atualizar e excluir notas, enquetes e mensagens',
'Criar, atualizar e excluir notas, enquetes e mensagens',
'oauth.scope.notifications:read.label': 'Ver notificações', 'oauth.scope.notifications:read.label': 'Ver notificações',
'oauth.scope.notifications:read.description': 'oauth.scope.notifications:read.description': 'Ler notificações e contagens não lidas',
'Ler notificações e contagens não lidas',
'oauth.scope.notifications:write.label': 'Gerenciar notificações', 'oauth.scope.notifications:write.label': 'Gerenciar notificações',
'oauth.scope.notifications:write.description': 'oauth.scope.notifications:write.description': 'Marcar notificações como lidas e respondê-las',
'Marcar notificações como lidas e respondê-las',
'oauth.scope.vacay:read.label': 'Ver planos de férias', 'oauth.scope.vacay:read.label': 'Ver planos de férias',
'oauth.scope.vacay:read.description': 'oauth.scope.vacay:read.description': 'Ler dados de planejamento de férias, entradas e estatísticas',
'Ler dados de planejamento de férias, entradas e estatísticas',
'oauth.scope.vacay:write.label': 'Gerenciar planos de férias', 'oauth.scope.vacay:write.label': 'Gerenciar planos de férias',
'oauth.scope.vacay:write.description': 'oauth.scope.vacay:write.description': 'Criar e gerenciar entradas de férias, feriados e planos de equipe',
'Criar e gerenciar entradas de férias, feriados e planos de equipe',
'oauth.scope.geo:read.label': 'Mapas e geocodificação', 'oauth.scope.geo:read.label': 'Mapas e geocodificação',
'oauth.scope.geo:read.description': 'oauth.scope.geo:read.description': 'Pesquisar locais, resolver URLs de mapa e geocodificar coordenadas',
'Pesquisar locais, resolver URLs de mapa e geocodificar coordenadas',
'oauth.scope.weather:read.label': 'Previsão do tempo', 'oauth.scope.weather:read.label': 'Previsão do tempo',
'oauth.scope.weather:read.description': 'oauth.scope.weather:read.description': 'Obter previsão do tempo para locais e datas da viagem',
'Obter previsão do tempo para locais e datas da viagem',
'oauth.scope.journey:read.label': 'Ver jornadas', 'oauth.scope.journey:read.label': 'Ver jornadas',
'oauth.scope.journey:read.description': 'oauth.scope.journey:read.description': 'Ler jornadas, entradas e lista de colaboradores',
'Ler jornadas, entradas e lista de colaboradores',
'oauth.scope.journey:write.label': 'Gerenciar jornadas', 'oauth.scope.journey:write.label': 'Gerenciar jornadas',
'oauth.scope.journey:write.description': 'oauth.scope.journey:write.description': 'Criar, atualizar e excluir jornadas e suas entradas',
'Criar, atualizar e excluir jornadas e suas entradas',
'oauth.scope.journey:share.label': 'Gerenciar links de jornadas', 'oauth.scope.journey:share.label': 'Gerenciar links de jornadas',
'oauth.scope.journey:share.description': 'oauth.scope.journey:share.description':
'Criar, atualizar e revogar links de compartilhamento públicos para jornadas', 'Criar, atualizar e revogar links de compartilhamento públicos para jornadas',
@@ -98,14 +73,11 @@ const oauth: TranslationStrings = {
'oauth.authorize.loading': 'Loading…', // en-fallback 'oauth.authorize.loading': 'Loading…', // en-fallback
'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback 'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback
'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback 'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback
'oauth.authorize.loginDescription': 'oauth.authorize.loginDescription': '{client} wants access to your TREK account. Please sign in first.', // en-fallback
'{client} wants access to your TREK account. Please sign in first.', // en-fallback
'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback 'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback
'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback 'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback
'oauth.authorize.requestDescription': 'oauth.authorize.requestDescription': 'This application is requesting access to your TREK account.', // en-fallback
'This application is requesting access to your TREK account.', // en-fallback 'oauth.authorize.trustNote': 'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.trustNote':
'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.selectScope': 'Select at least one scope', // en-fallback 'oauth.authorize.selectScope': 'Select at least one scope', // en-fallback
'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // en-fallback 'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // en-fallback
'oauth.authorize.approveManyScopes': 'Approve ({count} scopes)', // en-fallback 'oauth.authorize.approveManyScopes': 'Approve ({count} scopes)', // en-fallback
@@ -114,9 +86,7 @@ const oauth: TranslationStrings = {
'oauth.authorize.choosePermissions': 'Choose which permissions to grant', // en-fallback 'oauth.authorize.choosePermissions': 'Choose which permissions to grant', // en-fallback
'oauth.authorize.permissionsRequested': 'Permissions requested', // en-fallback 'oauth.authorize.permissionsRequested': 'Permissions requested', // en-fallback
'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback 'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback
'oauth.authorize.alwaysTool.listTrips': 'oauth.authorize.alwaysTool.listTrips': 'List your trips so the AI can discover trip IDs', // en-fallback
'List your trips so the AI can discover trip IDs', // en-fallback 'oauth.authorize.alwaysTool.getTripSummary': 'Read a trip overview needed to use any other tool', // en-fallback
'oauth.authorize.alwaysTool.getTripSummary':
'Read a trip overview needed to use any other tool', // en-fallback
}; };
export default oauth; export default oauth;
+2 -4
View File
@@ -5,8 +5,7 @@ const packing: TranslationStrings = {
'packing.empty': 'A lista de mala está vazia', 'packing.empty': 'A lista de mala está vazia',
'packing.import': 'Importar', 'packing.import': 'Importar',
'packing.importTitle': 'Importar lista de bagagem', 'packing.importTitle': 'Importar lista de bagagem',
'packing.importHint': 'packing.importHint': 'Um item por linha. Formato: Categoria, Nome, Peso (g), Bolsa, checked/unchecked (opcional)',
'Um item por linha. Formato: Categoria, Nome, Peso (g), Bolsa, checked/unchecked (opcional)',
'packing.importPlaceholder': 'packing.importPlaceholder':
'Higiene, Escova de dentes\nRoupas, Camisetas, 200\nDocumentos, Passaporte, , Mala de mão\nEletrônicos, Carregador, 50, Mala, checked', 'Higiene, Escova de dentes\nRoupas, Camisetas, 200\nDocumentos, Passaporte, , Mala de mão\nEletrônicos, Carregador, 50, Mala, checked',
'packing.importCsv': 'Carregar CSV/TXT', 'packing.importCsv': 'Carregar CSV/TXT',
@@ -52,8 +51,7 @@ const packing: TranslationStrings = {
'packing.addBag': 'Adicionar mala', 'packing.addBag': 'Adicionar mala',
'packing.changeCategory': 'Alterar categoria', 'packing.changeCategory': 'Alterar categoria',
'packing.confirm.clearChecked': 'Remover {count} item(ns) marcado(s)?', 'packing.confirm.clearChecked': 'Remover {count} item(ns) marcado(s)?',
'packing.confirm.deleteCat': 'packing.confirm.deleteCat': 'Excluir a categoria "{name}" com {count} item(ns)?',
'Excluir a categoria "{name}" com {count} item(ns)?',
'packing.defaultCategory': 'Outros', 'packing.defaultCategory': 'Outros',
'packing.toast.saveError': 'Falha ao salvar', 'packing.toast.saveError': 'Falha ao salvar',
'packing.toast.deleteError': 'Falha ao excluir', 'packing.toast.deleteError': 'Falha ao excluir',
+13 -26
View File
@@ -32,33 +32,20 @@ const perm: TranslationStrings = {
'perm.action.collab_edit': 'Colaboração (notas, enquetes, chat)', 'perm.action.collab_edit': 'Colaboração (notas, enquetes, chat)',
'perm.action.share_manage': 'Gerenciar links de compartilhamento', 'perm.action.share_manage': 'Gerenciar links de compartilhamento',
'perm.actionHint.trip_create': 'Quem pode criar novas viagens', 'perm.actionHint.trip_create': 'Quem pode criar novas viagens',
'perm.actionHint.trip_edit': 'perm.actionHint.trip_edit': 'Quem pode alterar nome, datas, descrição e moeda da viagem',
'Quem pode alterar nome, datas, descrição e moeda da viagem',
'perm.actionHint.trip_delete': 'Quem pode excluir permanentemente uma viagem', 'perm.actionHint.trip_delete': 'Quem pode excluir permanentemente uma viagem',
'perm.actionHint.trip_archive': 'perm.actionHint.trip_archive': 'Quem pode arquivar ou desarquivar uma viagem',
'Quem pode arquivar ou desarquivar uma viagem', 'perm.actionHint.trip_cover_upload': 'Quem pode enviar ou alterar a imagem de capa',
'perm.actionHint.trip_cover_upload': 'perm.actionHint.member_manage': 'Quem pode convidar ou remover membros da viagem',
'Quem pode enviar ou alterar a imagem de capa',
'perm.actionHint.member_manage':
'Quem pode convidar ou remover membros da viagem',
'perm.actionHint.file_upload': 'Quem pode enviar arquivos para uma viagem', 'perm.actionHint.file_upload': 'Quem pode enviar arquivos para uma viagem',
'perm.actionHint.file_edit': 'perm.actionHint.file_edit': 'Quem pode editar descrições e links dos arquivos',
'Quem pode editar descrições e links dos arquivos', 'perm.actionHint.file_delete': 'Quem pode mover arquivos para a lixeira ou excluí-los permanentemente',
'perm.actionHint.file_delete': 'perm.actionHint.place_edit': 'Quem pode adicionar, editar ou excluir lugares',
'Quem pode mover arquivos para a lixeira ou excluí-los permanentemente', 'perm.actionHint.day_edit': 'Quem pode editar dias, notas dos dias e atribuições de lugares',
'perm.actionHint.place_edit': 'perm.actionHint.reservation_edit': 'Quem pode criar, editar ou excluir reservas',
'Quem pode adicionar, editar ou excluir lugares', 'perm.actionHint.budget_edit': 'Quem pode criar, editar ou excluir itens do orçamento',
'perm.actionHint.day_edit': 'perm.actionHint.packing_edit': 'Quem pode gerenciar itens de bagagem e malas',
'Quem pode editar dias, notas dos dias e atribuições de lugares', 'perm.actionHint.collab_edit': 'Quem pode criar notas, enquetes e enviar mensagens',
'perm.actionHint.reservation_edit': 'perm.actionHint.share_manage': 'Quem pode criar ou excluir links de compartilhamento públicos',
'Quem pode criar, editar ou excluir reservas',
'perm.actionHint.budget_edit':
'Quem pode criar, editar ou excluir itens do orçamento',
'perm.actionHint.packing_edit':
'Quem pode gerenciar itens de bagagem e malas',
'perm.actionHint.collab_edit':
'Quem pode criar notas, enquetes e enviar mensagens',
'perm.actionHint.share_manage':
'Quem pode criar ou excluir links de compartilhamento públicos',
}; };
export default perm; export default perm;
+7 -14
View File
@@ -6,13 +6,10 @@ const places: TranslationStrings = {
'places.sidebarDrop': 'Solte para importar', 'places.sidebarDrop': 'Solte para importar',
'places.importFileHint': 'places.importFileHint':
'Importe arquivos .gpx, .kml ou .kmz de ferramentas como Google My Maps, Google Earth ou um rastreador GPS.', 'Importe arquivos .gpx, .kml ou .kmz de ferramentas como Google My Maps, Google Earth ou um rastreador GPS.',
'places.importFileDropHere': 'places.importFileDropHere': 'Clique para selecionar um arquivo ou arraste e solte aqui',
'Clique para selecionar um arquivo ou arraste e solte aqui',
'places.importFileDropActive': 'Solte o arquivo para selecionar', 'places.importFileDropActive': 'Solte o arquivo para selecionar',
'places.importFileUnsupported': 'places.importFileUnsupported': 'Tipo de arquivo não suportado. Use .gpx, .kml ou .kmz.',
'Tipo de arquivo não suportado. Use .gpx, .kml ou .kmz.', 'places.importFileTooLarge': 'O arquivo é muito grande. O tamanho máximo de upload é {maxMb} MB.',
'places.importFileTooLarge':
'O arquivo é muito grande. O tamanho máximo de upload é {maxMb} MB.',
'places.importFileError': 'Importação falhou', 'places.importFileError': 'Importação falhou',
'places.importAllSkipped': 'Todos os lugares já estavam na viagem.', 'places.importAllSkipped': 'Todos os lugares já estavam na viagem.',
'places.gpxImported': '{count} lugares importados do GPX', 'places.gpxImported': '{count} lugares importados do GPX',
@@ -30,16 +27,13 @@ const places: TranslationStrings = {
'places.kmlKmzImported': '{count} lugares importados de KMZ/KML', 'places.kmlKmzImported': '{count} lugares importados de KMZ/KML',
'places.urlResolved': 'Lugar importado da URL', 'places.urlResolved': 'Lugar importado da URL',
'places.importList': 'Importar lista', 'places.importList': 'Importar lista',
'places.kmlKmzSummaryValues': 'places.kmlKmzSummaryValues': 'Placemarks: {total} • Importados: {created} • Ignorados: {skipped}',
'Placemarks: {total} • Importados: {created} • Ignorados: {skipped}',
'places.importGoogleList': 'Lista Google', 'places.importGoogleList': 'Lista Google',
'places.importNaverList': 'Lista Naver', 'places.importNaverList': 'Lista Naver',
'places.googleListHint': 'places.googleListHint': 'Cole um link compartilhado de uma lista do Google Maps para importar todos os lugares.',
'Cole um link compartilhado de uma lista do Google Maps para importar todos os lugares.',
'places.googleListImported': '{count} lugares importados de "{list}"', 'places.googleListImported': '{count} lugares importados de "{list}"',
'places.googleListError': 'Falha ao importar lista do Google Maps', 'places.googleListError': 'Falha ao importar lista do Google Maps',
'places.naverListHint': 'places.naverListHint': 'Cole um link compartilhado de uma lista do Naver Maps para importar todos os lugares.',
'Cole um link compartilhado de uma lista do Naver Maps para importar todos os lugares.',
'places.naverListImported': '{count} lugares importados de "{list}"', 'places.naverListImported': '{count} lugares importados de "{list}"',
'places.naverListError': 'Falha ao importar lista do Naver Maps', 'places.naverListError': 'Falha ao importar lista do Naver Maps',
'places.viewDetails': 'Ver detalhes', 'places.viewDetails': 'Ver detalhes',
@@ -76,8 +70,7 @@ const places: TranslationStrings = {
'places.formNotes': 'Notas', 'places.formNotes': 'Notas',
'places.formNotesPlaceholder': 'Notas pessoais...', 'places.formNotesPlaceholder': 'Notas pessoais...',
'places.formReservation': 'Reserva', 'places.formReservation': 'Reserva',
'places.reservationNotesPlaceholder': 'places.reservationNotesPlaceholder': 'Notas da reserva, código de confirmação...',
'Notas da reserva, código de confirmação...',
'places.mapsSearchPlaceholder': 'Buscar lugares...', 'places.mapsSearchPlaceholder': 'Buscar lugares...',
'places.mapsSearchError': 'Falha na busca de lugares.', 'places.mapsSearchError': 'Falha na busca de lugares.',
'places.loadingDetails': 'Carregando detalhes do lugar…', 'places.loadingDetails': 'Carregando detalhes do lugar…',
+3 -6
View File
@@ -7,8 +7,7 @@ const planner: TranslationStrings = {
'planner.documents': 'Documentos', 'planner.documents': 'Documentos',
'planner.dayPlan': 'Plano do dia', 'planner.dayPlan': 'Plano do dia',
'planner.reservations': 'Reservas', 'planner.reservations': 'Reservas',
'planner.minTwoPlaces': 'planner.minTwoPlaces': 'São necessários pelo menos 2 lugares com coordenadas',
'São necessários pelo menos 2 lugares com coordenadas',
'planner.noGeoPlaces': 'Nenhum lugar com coordenadas disponível', 'planner.noGeoPlaces': 'Nenhum lugar com coordenadas disponível',
'planner.routeCalculated': 'Rota calculada', 'planner.routeCalculated': 'Rota calculada',
'planner.routeCalcFailed': 'Não foi possível calcular a rota', 'planner.routeCalcFailed': 'Não foi possível calcular a rota',
@@ -34,8 +33,7 @@ const planner: TranslationStrings = {
'planner.resConfirmed': 'Reserva confirmada · ', 'planner.resConfirmed': 'Reserva confirmada · ',
'planner.notePlaceholder': 'Nota…', 'planner.notePlaceholder': 'Nota…',
'planner.noteTimePlaceholder': 'Horário (opcional)', 'planner.noteTimePlaceholder': 'Horário (opcional)',
'planner.noteExamplePlaceholder': 'planner.noteExamplePlaceholder': 'ex.: metrô às 14:30 da estação central, barco do cais 7, pausa para almoço…',
'ex.: metrô às 14:30 da estação central, barco do cais 7, pausa para almoço…',
'planner.totalCost': 'Custo total', 'planner.totalCost': 'Custo total',
'planner.searchPlaces': 'Buscar lugares…', 'planner.searchPlaces': 'Buscar lugares…',
'planner.allCategories': 'Todas as categorias', 'planner.allCategories': 'Todas as categorias',
@@ -49,8 +47,7 @@ const planner: TranslationStrings = {
'planner.route': 'Rota', 'planner.route': 'Rota',
'planner.optimize': 'Otimizar', 'planner.optimize': 'Otimizar',
'planner.openGoogleMaps': 'Abrir no Google Maps', 'planner.openGoogleMaps': 'Abrir no Google Maps',
'planner.selectDayHint': 'planner.selectDayHint': 'Selecione um dia na lista à esquerda para ver o plano do dia',
'Selecione um dia na lista à esquerda para ver o plano do dia',
'planner.noPlacesForDay': 'Nenhum lugar neste dia ainda', 'planner.noPlacesForDay': 'Nenhum lugar neste dia ainda',
'planner.addPlacesLink': 'Adicionar lugares →', 'planner.addPlacesLink': 'Adicionar lugares →',
'planner.minTotal': 'mín. total', 'planner.minTotal': 'mín. total',
+18 -36
View File
@@ -6,8 +6,7 @@ const reservations: TranslationStrings = {
'reservations.emptyHint': 'Adicione reservas de voos, hotéis e mais', 'reservations.emptyHint': 'Adicione reservas de voos, hotéis e mais',
'reservations.add': 'Adicionar reserva', 'reservations.add': 'Adicionar reserva',
'reservations.addManual': 'Reserva manual', 'reservations.addManual': 'Reserva manual',
'reservations.placeHint': 'reservations.placeHint': 'Dica: o ideal é criar reservas a partir de um lugar para vinculá-las ao plano do dia.',
'Dica: o ideal é criar reservas a partir de um lugar para vinculá-las ao plano do dia.',
'reservations.confirmed': 'Confirmada', 'reservations.confirmed': 'Confirmada',
'reservations.pending': 'Pendente', 'reservations.pending': 'Pendente',
'reservations.summary': '{confirmed} confirmada(s), {pending} pendente(s)', 'reservations.summary': '{confirmed} confirmada(s), {pending} pendente(s)',
@@ -33,8 +32,7 @@ const reservations: TranslationStrings = {
'reservations.layover.connection': 'Conexão', 'reservations.layover.connection': 'Conexão',
'reservations.layover.layover': 'Escala', 'reservations.layover.layover': 'Escala',
'reservations.needsReview': 'Verificar', 'reservations.needsReview': 'Verificar',
'reservations.needsReviewHint': 'reservations.needsReviewHint': 'Aeroporto não pôde ser identificado automaticamente — confirme o local.',
'Aeroporto não pôde ser identificado automaticamente — confirme o local.',
'reservations.searchLocation': 'Buscar estação, porto, endereço...', 'reservations.searchLocation': 'Buscar estação, porto, endereço...',
'reservations.meta.trainNumber': 'Nº do trem', 'reservations.meta.trainNumber': 'Nº do trem',
'reservations.meta.platform': 'Plataforma', 'reservations.meta.platform': 'Plataforma',
@@ -64,8 +62,7 @@ const reservations: TranslationStrings = {
'reservations.type.bicycle': 'Bicicleta', 'reservations.type.bicycle': 'Bicicleta',
'reservations.type.taxi': 'Táxi', 'reservations.type.taxi': 'Táxi',
'reservations.type.transport_other': 'Outro', 'reservations.type.transport_other': 'Outro',
'reservations.confirm.delete': 'reservations.confirm.delete': 'Tem certeza de que deseja excluir a reserva "{name}"?',
'Tem certeza de que deseja excluir a reserva "{name}"?',
'reservations.confirm.deleteTitle': 'Excluir reserva?', 'reservations.confirm.deleteTitle': 'Excluir reserva?',
'reservations.confirm.deleteBody': '"{name}" será excluído permanentemente.', 'reservations.confirm.deleteBody': '"{name}" será excluído permanentemente.',
'reservations.toast.updated': 'Reserva atualizada', 'reservations.toast.updated': 'Reserva atualizada',
@@ -99,8 +96,7 @@ const reservations: TranslationStrings = {
'reservations.budgetCategory': 'Categoria de orçamento', 'reservations.budgetCategory': 'Categoria de orçamento',
'reservations.budgetCategoryPlaceholder': 'ex. Transporte, Acomodação', 'reservations.budgetCategoryPlaceholder': 'ex. Transporte, Acomodação',
'reservations.budgetCategoryAuto': 'Automático (pelo tipo de reserva)', 'reservations.budgetCategoryAuto': 'Automático (pelo tipo de reserva)',
'reservations.budgetHint': 'reservations.budgetHint': 'Uma entrada de orçamento será criada automaticamente ao salvar.',
'Uma entrada de orçamento será criada automaticamente ao salvar.',
'reservations.departureDate': 'Partida', 'reservations.departureDate': 'Partida',
'reservations.arrivalDate': 'Chegada', 'reservations.arrivalDate': 'Chegada',
'reservations.departureTime': 'Hora partida', 'reservations.departureTime': 'Hora partida',
@@ -121,60 +117,46 @@ const reservations: TranslationStrings = {
'reservations.span.start': 'Início', 'reservations.span.start': 'Início',
'reservations.span.end': 'Fim', 'reservations.span.end': 'Fim',
'reservations.span.ongoing': 'Em andamento', 'reservations.span.ongoing': 'Em andamento',
'reservations.validation.endBeforeStart': 'reservations.validation.endBeforeStart': 'A data/hora final deve ser posterior à data/hora inicial',
'A data/hora final deve ser posterior à data/hora inicial',
'reservations.addBooking': 'Adicionar reserva', 'reservations.addBooking': 'Adicionar reserva',
'reservations.import.title': 'Importar confirmações de reserva', 'reservations.import.title': 'Importar confirmações de reserva',
'reservations.import.cta': 'Importar de arquivo', 'reservations.import.cta': 'Importar de arquivo',
'reservations.import.dropHere': 'reservations.import.dropHere': 'Solte os arquivos de confirmação de reserva aqui ou clique para selecionar',
'Solte os arquivos de confirmação de reserva aqui ou clique para selecionar',
'reservations.import.dropActive': 'Solte os arquivos para importar', 'reservations.import.dropActive': 'Solte os arquivos para importar',
'reservations.import.acceptedFormats': 'reservations.import.acceptedFormats': 'Aceitos: EML, PDF, PKPass, HTML, TXT (máx. 10 MB cada, até 5 arquivos)',
'Aceitos: EML, PDF, PKPass, HTML, TXT (máx. 10 MB cada, até 5 arquivos)',
'reservations.import.parsing': 'Analisando arquivos…', 'reservations.import.parsing': 'Analisando arquivos…',
'reservations.import.previewHeading': '{count} reserva(s) encontrada(s)', 'reservations.import.previewHeading': '{count} reserva(s) encontrada(s)',
'reservations.import.previewEmpty': 'reservations.import.previewEmpty': 'Nenhuma reserva pôde ser extraída dos arquivos enviados.',
'Nenhuma reserva pôde ser extraída dos arquivos enviados.',
'reservations.import.removeItem': 'Remover', 'reservations.import.removeItem': 'Remover',
'reservations.import.confirm': 'Importar {count} reserva(s)', 'reservations.import.confirm': 'Importar {count} reserva(s)',
'reservations.import.back': 'Voltar', 'reservations.import.back': 'Voltar',
'reservations.import.success': '{count} reserva(s) importada(s)', 'reservations.import.success': '{count} reserva(s) importada(s)',
'reservations.import.partialFailure': 'reservations.import.partialFailure': '{created} importada(s), {failed} falhou/falharam',
'{created} importada(s), {failed} falhou/falharam', 'reservations.import.error': 'Falha na análise. Verifique se o arquivo é uma confirmação de reserva válida.',
'reservations.import.error': 'reservations.import.unavailable': 'A importação de reservas não está disponível neste servidor.',
'Falha na análise. Verifique se o arquivo é uma confirmação de reserva válida.', 'reservations.import.unsupportedFormat': 'Formato de arquivo não suportado. Use EML, PDF, PKPass, HTML ou TXT.',
'reservations.import.unavailable': 'reservations.import.fileTooLarge': 'O arquivo "{name}" excede o limite de 10 MB.',
'A importação de reservas não está disponível neste servidor.',
'reservations.import.unsupportedFormat':
'Formato de arquivo não suportado. Use EML, PDF, PKPass, HTML ou TXT.',
'reservations.import.fileTooLarge':
'O arquivo "{name}" excede o limite de 10 MB.',
'reservations.airtrail.title': 'Importar do AirTrail', 'reservations.airtrail.title': 'Importar do AirTrail',
'reservations.airtrail.cta': 'AirTrail', 'reservations.airtrail.cta': 'AirTrail',
'reservations.airtrail.synced': 'AirTrail', 'reservations.airtrail.synced': 'AirTrail',
'reservations.airtrail.syncedHint': 'reservations.airtrail.syncedHint':
'Sincronizado do AirTrail — as edições permanecem em sincronia nos dois sentidos.', 'Sincronizado do AirTrail — as edições permanecem em sincronia nos dois sentidos.',
'reservations.airtrail.notSynced': 'Não sincronizado', 'reservations.airtrail.notSynced': 'Não sincronizado',
'reservations.airtrail.notSyncedHint': 'reservations.airtrail.notSyncedHint': 'Este voo foi removido no AirTrail e não sincroniza mais.',
'Este voo foi removido no AirTrail e não sincroniza mais.', 'reservations.airtrail.loadError': 'Não foi possível carregar seus voos do AirTrail.',
'reservations.airtrail.loadError':
'Não foi possível carregar seus voos do AirTrail.',
'reservations.airtrail.imported': '{count} voo(s) importado(s)', 'reservations.airtrail.imported': '{count} voo(s) importado(s)',
'reservations.airtrail.skippedDuplicate': 'reservations.airtrail.skippedDuplicate': '{count} já nesta viagem, ignorado(s)',
'{count} já nesta viagem, ignorado(s)',
'reservations.airtrail.nothingImported': 'Nada para importar.', 'reservations.airtrail.nothingImported': 'Nada para importar.',
'reservations.airtrail.importError': 'Falha na importação. Tente novamente.', 'reservations.airtrail.importError': 'Falha na importação. Tente novamente.',
'reservations.airtrail.undo': 'Importar do AirTrail', 'reservations.airtrail.undo': 'Importar do AirTrail',
'reservations.airtrail.alreadyImported': 'Importado', 'reservations.airtrail.alreadyImported': 'Importado',
'reservations.airtrail.duringTrip': 'Durante esta viagem', 'reservations.airtrail.duringTrip': 'Durante esta viagem',
'reservations.airtrail.otherFlights': 'Outros voos', 'reservations.airtrail.otherFlights': 'Outros voos',
'reservations.airtrail.empty': 'reservations.airtrail.empty': 'Nenhum voo encontrado na sua conta do AirTrail.',
'Nenhum voo encontrado na sua conta do AirTrail.',
'reservations.airtrail.importCta': 'Importar {count}', 'reservations.airtrail.importCta': 'Importar {count}',
'reservations.costsLabel': 'Costs', 'reservations.costsLabel': 'Costs',
'reservations.createExpense': 'Create expense', 'reservations.createExpense': 'Create expense',
'reservations.createExpenseHint': 'reservations.createExpenseHint': 'Saves the booking, then opens the Costs editor.',
'Saves the booking, then opens the Costs editor.',
'reservations.linkedExpense': 'Linked expense', 'reservations.linkedExpense': 'Linked expense',
'reservations.removeExpense': 'Remove expense', 'reservations.removeExpense': 'Remove expense',
}; };
+32 -53
View File
@@ -14,12 +14,10 @@ const settings: TranslationStrings = {
'settings.mapTemplate': 'Modelo de mapa', 'settings.mapTemplate': 'Modelo de mapa',
'settings.mapTemplatePlaceholder.select': 'Selecione o modelo...', 'settings.mapTemplatePlaceholder.select': 'Selecione o modelo...',
'settings.mapDefaultHint': 'Deixe vazio para OpenStreetMap (padrão)', 'settings.mapDefaultHint': 'Deixe vazio para OpenStreetMap (padrão)',
'settings.mapTemplatePlaceholder': 'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'settings.mapHint': 'URL do modelo de blocos do mapa', 'settings.mapHint': 'URL do modelo de blocos do mapa',
'settings.mapProvider': 'Provedor de mapa', 'settings.mapProvider': 'Provedor de mapa',
'settings.mapProviderHint': 'settings.mapProviderHint': 'Afeta os mapas do Planejador de Viagem e Diário. Atlas sempre usa Leaflet.',
'Afeta os mapas do Planejador de Viagem e Diário. Atlas sempre usa Leaflet.',
'settings.mapLeafletSubtitle': 'Clássico 2D, quaisquer blocos raster', 'settings.mapLeafletSubtitle': 'Clássico 2D, quaisquer blocos raster',
'settings.mapMapboxSubtitle': 'Blocos vetoriais, prédios 3D & terreno', 'settings.mapMapboxSubtitle': 'Blocos vetoriais, prédios 3D & terreno',
'settings.mapExperimental': 'Experimental', 'settings.mapExperimental': 'Experimental',
@@ -30,13 +28,11 @@ const settings: TranslationStrings = {
'settings.mapStylePlaceholder': 'Selecionar um estilo Mapbox', 'settings.mapStylePlaceholder': 'Selecionar um estilo Mapbox',
'settings.mapStyleHint': 'Preset ou sua própria URL mapbox://styles/USER/ID', 'settings.mapStyleHint': 'Preset ou sua própria URL mapbox://styles/USER/ID',
'settings.map3dBuildings': 'Prédios 3D & terreno', 'settings.map3dBuildings': 'Prédios 3D & terreno',
'settings.map3dHint': 'settings.map3dHint': 'Inclinação + extrusões 3D reais de prédios — funciona em todo estilo, incluindo satélite.',
'Inclinação + extrusões 3D reais de prédios — funciona em todo estilo, incluindo satélite.',
'settings.mapHighQuality': 'Modo alta qualidade', 'settings.mapHighQuality': 'Modo alta qualidade',
'settings.mapHighQualityHint': 'settings.mapHighQualityHint':
'Antialiasing + projeção global para bordas mais nítidas e uma visão realista do mundo.', 'Antialiasing + projeção global para bordas mais nítidas e uma visão realista do mundo.',
'settings.mapHighQualityWarning': 'settings.mapHighQualityWarning': 'Pode afetar o desempenho em dispositivos menos potentes.',
'Pode afetar o desempenho em dispositivos menos potentes.',
'settings.mapTipLabel': 'Dica:', 'settings.mapTipLabel': 'Dica:',
'settings.mapTip': 'settings.mapTip':
'Clique direito e arraste para girar/inclinar o mapa. Clique do meio para adicionar um local (o clique direito é reservado para rotação).', 'Clique direito e arraste para girar/inclinar o mapa. Clique do meio para adicionar um local (o clique direito é reservado para rotação).',
@@ -45,11 +41,9 @@ const settings: TranslationStrings = {
'settings.saveMap': 'Salvar mapa', 'settings.saveMap': 'Salvar mapa',
'settings.apiKeys': 'Chaves de API', 'settings.apiKeys': 'Chaves de API',
'settings.mapsKey': 'Chave da API Google Maps', 'settings.mapsKey': 'Chave da API Google Maps',
'settings.mapsKeyHint': 'settings.mapsKeyHint': 'Para busca de lugares. Requer Places API (New). Obtenha em console.cloud.google.com',
'Para busca de lugares. Requer Places API (New). Obtenha em console.cloud.google.com',
'settings.weatherKey': 'Chave OpenWeatherMap', 'settings.weatherKey': 'Chave OpenWeatherMap',
'settings.weatherKeyHint': 'settings.weatherKeyHint': 'Para dados meteorológicos. Grátis em openweathermap.org/api',
'Para dados meteorológicos. Grátis em openweathermap.org/api',
'settings.keyPlaceholder': 'Digite a chave...', 'settings.keyPlaceholder': 'Digite a chave...',
'settings.configured': 'Configurada', 'settings.configured': 'Configurada',
'settings.saveKeys': 'Salvar chaves', 'settings.saveKeys': 'Salvar chaves',
@@ -78,8 +72,7 @@ const settings: TranslationStrings = {
'settings.notificationsDisabled': 'settings.notificationsDisabled':
'As notificações não estão configuradas. Peça a um administrador para ativar notificações por e-mail ou webhook.', 'As notificações não estão configuradas. Peça a um administrador para ativar notificações por e-mail ou webhook.',
'settings.notificationsActive': 'Canal ativo', 'settings.notificationsActive': 'Canal ativo',
'settings.notificationsManagedByAdmin': 'settings.notificationsManagedByAdmin': 'Os eventos de notificação são configurados pelo administrador.',
'Os eventos de notificação são configurados pelo administrador.',
'settings.on': 'Ligado', 'settings.on': 'Ligado',
'settings.off': 'Desligado', 'settings.off': 'Desligado',
'settings.account': 'Conta', 'settings.account': 'Conta',
@@ -97,15 +90,13 @@ const settings: TranslationStrings = {
'settings.about.supporters.tierEmpty': 'Seja o primeiro', 'settings.about.supporters.tierEmpty': 'Seja o primeiro',
'settings.about.supporter.tier.noReturnTicket': 'No Return Ticket', 'settings.about.supporter.tier.noReturnTicket': 'No Return Ticket',
'settings.about.supporter.tier.lostLuggageVip': 'Lost Luggage VIP', 'settings.about.supporter.tier.lostLuggageVip': 'Lost Luggage VIP',
'settings.about.supporter.tier.businessClassDreamer': 'settings.about.supporter.tier.businessClassDreamer': 'Business Class Dreamer',
'Business Class Dreamer',
'settings.about.supporter.tier.budgetTraveller': 'Budget Traveller', 'settings.about.supporter.tier.budgetTraveller': 'Budget Traveller',
'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate', 'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate',
'settings.about.description': 'settings.about.description':
'TREK é um planejador de viagens auto-hospedado que ajuda você a organizar suas viagens da primeira ideia à última lembrança. Planejamento diário, orçamento, listas de bagagem, fotos e muito mais — tudo em um só lugar, no seu próprio servidor.', 'TREK é um planejador de viagens auto-hospedado que ajuda você a organizar suas viagens da primeira ideia à última lembrança. Planejamento diário, orçamento, listas de bagagem, fotos e muito mais — tudo em um só lugar, no seu próprio servidor.',
'settings.about.madeWith': 'Feito com', 'settings.about.madeWith': 'Feito com',
'settings.about.madeBy': 'settings.about.madeBy': 'por Maurice e uma crescente comunidade open-source.',
'por Maurice e uma crescente comunidade open-source.',
'settings.username': 'Nome de usuário', 'settings.username': 'Nome de usuário',
'settings.email': 'E-mail', 'settings.email': 'E-mail',
'settings.role': 'Função', 'settings.role': 'Função',
@@ -120,8 +111,7 @@ const settings: TranslationStrings = {
'settings.passwordRequired': 'Informe a senha atual e a nova', 'settings.passwordRequired': 'Informe a senha atual e a nova',
'settings.passwordTooShort': 'A senha deve ter pelo menos 8 caracteres', 'settings.passwordTooShort': 'A senha deve ter pelo menos 8 caracteres',
'settings.passwordMismatch': 'As senhas não coincidem', 'settings.passwordMismatch': 'As senhas não coincidem',
'settings.passwordWeak': 'settings.passwordWeak': 'A senha deve ter maiúscula, minúscula, número e um caractere especial',
'A senha deve ter maiúscula, minúscula, número e um caractere especial',
'settings.passwordChanged': 'Senha alterada com sucesso', 'settings.passwordChanged': 'Senha alterada com sucesso',
'settings.deleteAccount': 'Excluir conta', 'settings.deleteAccount': 'Excluir conta',
'settings.deleteAccountTitle': 'Excluir sua conta?', 'settings.deleteAccountTitle': 'Excluir sua conta?',
@@ -148,10 +138,8 @@ const settings: TranslationStrings = {
'settings.mfa.requiredByPolicy': 'settings.mfa.requiredByPolicy':
'O administrador exige autenticação em dois fatores. Configure um app autenticador abaixo antes de continuar.', 'O administrador exige autenticação em dois fatores. Configure um app autenticador abaixo antes de continuar.',
'settings.mfa.backupTitle': 'Códigos de backup', 'settings.mfa.backupTitle': 'Códigos de backup',
'settings.mfa.backupDescription': 'settings.mfa.backupDescription': 'Use estes códigos únicos se perder acesso ao app autenticador.',
'Use estes códigos únicos se perder acesso ao app autenticador.', 'settings.mfa.backupWarning': 'Salve estes códigos agora. Cada código pode ser usado apenas uma vez.',
'settings.mfa.backupWarning':
'Salve estes códigos agora. Cada código pode ser usado apenas uma vez.',
'settings.mfa.backupCopy': 'Copiar códigos', 'settings.mfa.backupCopy': 'Copiar códigos',
'settings.mfa.backupDownload': 'Baixar TXT', 'settings.mfa.backupDownload': 'Baixar TXT',
'settings.mfa.backupPrint': 'Imprimir / PDF', 'settings.mfa.backupPrint': 'Imprimir / PDF',
@@ -159,15 +147,13 @@ const settings: TranslationStrings = {
'settings.mfa.enabled': 'O 2FA está ativado na sua conta.', 'settings.mfa.enabled': 'O 2FA está ativado na sua conta.',
'settings.mfa.disabled': 'O 2FA não está ativado.', 'settings.mfa.disabled': 'O 2FA não está ativado.',
'settings.mfa.setup': 'Configurar autenticador', 'settings.mfa.setup': 'Configurar autenticador',
'settings.mfa.scanQr': 'settings.mfa.scanQr': 'Leia este QR code no app ou digite o segredo manualmente.',
'Leia este QR code no app ou digite o segredo manualmente.',
'settings.mfa.secretLabel': 'Chave secreta (entrada manual)', 'settings.mfa.secretLabel': 'Chave secreta (entrada manual)',
'settings.mfa.codePlaceholder': 'Código de 6 dígitos', 'settings.mfa.codePlaceholder': 'Código de 6 dígitos',
'settings.mfa.enable': 'Ativar 2FA', 'settings.mfa.enable': 'Ativar 2FA',
'settings.mfa.cancelSetup': 'Cancelar', 'settings.mfa.cancelSetup': 'Cancelar',
'settings.mfa.disableTitle': 'Desativar 2FA', 'settings.mfa.disableTitle': 'Desativar 2FA',
'settings.mfa.disableHint': 'settings.mfa.disableHint': 'Digite sua senha e um código atual do autenticador.',
'Digite sua senha e um código atual do autenticador.',
'settings.mfa.disable': 'Desativar 2FA', 'settings.mfa.disable': 'Desativar 2FA',
'settings.mfa.toastEnabled': 'Autenticação em duas etapas ativada', 'settings.mfa.toastEnabled': 'Autenticação em duas etapas ativada',
'settings.mfa.toastDisabled': 'Autenticação em duas etapas desativada', 'settings.mfa.toastDisabled': 'Autenticação em duas etapas desativada',
@@ -183,8 +169,7 @@ const settings: TranslationStrings = {
'settings.mcp.copied': 'Copiado!', 'settings.mcp.copied': 'Copiado!',
'settings.mcp.apiTokens': 'Tokens de API', 'settings.mcp.apiTokens': 'Tokens de API',
'settings.mcp.createToken': 'Criar novo token', 'settings.mcp.createToken': 'Criar novo token',
'settings.mcp.noTokens': 'settings.mcp.noTokens': 'Nenhum token ainda. Crie um para conectar clientes MCP.',
'Nenhum token ainda. Crie um para conectar clientes MCP.',
'settings.mcp.tokenCreatedAt': 'Criado em', 'settings.mcp.tokenCreatedAt': 'Criado em',
'settings.mcp.tokenUsedAt': 'Usado em', 'settings.mcp.tokenUsedAt': 'Usado em',
'settings.mcp.deleteTokenTitle': 'Excluir token', 'settings.mcp.deleteTokenTitle': 'Excluir token',
@@ -192,8 +177,7 @@ const settings: TranslationStrings = {
'Este token deixará de funcionar imediatamente. Qualquer cliente MCP que o utilize perderá o acesso.', 'Este token deixará de funcionar imediatamente. Qualquer cliente MCP que o utilize perderá o acesso.',
'settings.mcp.modal.createTitle': 'Criar token de API', 'settings.mcp.modal.createTitle': 'Criar token de API',
'settings.mcp.modal.tokenName': 'Nome do token', 'settings.mcp.modal.tokenName': 'Nome do token',
'settings.mcp.modal.tokenNamePlaceholder': 'settings.mcp.modal.tokenNamePlaceholder': 'ex.: Claude Desktop, Notebook do trabalho',
'ex.: Claude Desktop, Notebook do trabalho',
'settings.mcp.modal.creating': 'Criando…', 'settings.mcp.modal.creating': 'Criando…',
'settings.mcp.modal.create': 'Criar token', 'settings.mcp.modal.create': 'Criar token',
'settings.mcp.modal.createdTitle': 'Token criado', 'settings.mcp.modal.createdTitle': 'Token criado',
@@ -229,15 +213,13 @@ const settings: TranslationStrings = {
'settings.oauth.sessionExpires': 'Expira', 'settings.oauth.sessionExpires': 'Expira',
'settings.oauth.revoke': 'Revogar', 'settings.oauth.revoke': 'Revogar',
'settings.oauth.revokeSession': 'Revogar sessão', 'settings.oauth.revokeSession': 'Revogar sessão',
'settings.oauth.revokeSessionMessage': 'settings.oauth.revokeSessionMessage': 'Isso revogará imediatamente o acesso desta sessão OAuth.',
'Isso revogará imediatamente o acesso desta sessão OAuth.',
'settings.oauth.modal.createTitle': 'Registrar cliente OAuth', 'settings.oauth.modal.createTitle': 'Registrar cliente OAuth',
'settings.oauth.modal.presets': 'Configurações rápidas', 'settings.oauth.modal.presets': 'Configurações rápidas',
'settings.oauth.modal.clientName': 'Nome da aplicação', 'settings.oauth.modal.clientName': 'Nome da aplicação',
'settings.oauth.modal.clientNamePlaceholder': 'ex.: Claude Web, Meu app MCP', 'settings.oauth.modal.clientNamePlaceholder': 'ex.: Claude Web, Meu app MCP',
'settings.oauth.modal.redirectUris': 'URIs de redirecionamento', 'settings.oauth.modal.redirectUris': 'URIs de redirecionamento',
'settings.oauth.modal.redirectUrisPlaceholder': 'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
'https://your-app.com/callback\nhttps://your-app.com/auth',
'settings.oauth.modal.redirectUrisHint': 'settings.oauth.modal.redirectUrisHint':
'Uma URI por linha. HTTPS obrigatório (localhost isento). Correspondência exata.', 'Uma URI por linha. HTTPS obrigatório (localhost isento). Correspondência exata.',
'settings.oauth.modal.scopes': 'Escopos permitidos', 'settings.oauth.modal.scopes': 'Escopos permitidos',
@@ -256,18 +238,15 @@ const settings: TranslationStrings = {
'settings.oauth.toast.revoked': 'Sessão revogada', 'settings.oauth.toast.revoked': 'Sessão revogada',
'settings.oauth.toast.revokeError': 'Falha ao revogar sessão', 'settings.oauth.toast.revokeError': 'Falha ao revogar sessão',
'settings.oauth.toast.rotateError': 'Falha ao renovar segredo do cliente', 'settings.oauth.toast.rotateError': 'Falha ao renovar segredo do cliente',
'settings.oauth.modal.machineClient': 'settings.oauth.modal.machineClient': 'Cliente de máquina (sem login no navegador)',
'Cliente de máquina (sem login no navegador)',
'settings.oauth.modal.machineClientHint': 'settings.oauth.modal.machineClientHint':
'Usa o grant client_credentials — sem URIs de redirecionamento. O token é emitido diretamente via client_id + client_secret e age como você dentro dos escopos selecionados.', 'Usa o grant client_credentials — sem URIs de redirecionamento. O token é emitido diretamente via client_id + client_secret e age como você dentro dos escopos selecionados.',
'settings.oauth.modal.machineClientUsage': 'settings.oauth.modal.machineClientUsage':
'Obter token: POST /oauth/token com grant_type=client_credentials, client_id e client_secret. Sem navegador, sem refresh token.', 'Obter token: POST /oauth/token com grant_type=client_credentials, client_id e client_secret. Sem navegador, sem refresh token.',
'settings.oauth.badge.machine': 'máquina', 'settings.oauth.badge.machine': 'máquina',
'settings.mustChangePassword': 'settings.mustChangePassword': 'Você deve alterar sua senha antes de continuar. Defina uma nova senha abaixo.',
'Você deve alterar sua senha antes de continuar. Defina uma nova senha abaixo.',
'settings.bookingLabels': 'Rótulos das rotas de reservas', 'settings.bookingLabels': 'Rótulos das rotas de reservas',
'settings.bookingLabelsHint': 'settings.bookingLabelsHint': 'Mostra nomes de estações / aeroportos no mapa. Desativado, apenas o ícone aparece.',
'Mostra nomes de estações / aeroportos no mapa. Desativado, apenas o ícone aparece.',
'settings.notifyVersionAvailable': 'Nova versão disponível', 'settings.notifyVersionAvailable': 'Nova versão disponível',
'settings.notificationPreferences.noChannels': 'settings.notificationPreferences.noChannels':
'Nenhum canal de notificação configurado. Peça a um administrador para configurar notificações por e-mail ou webhook.', 'Nenhum canal de notificação configurado. Peça a um administrador para configurar notificações por e-mail ou webhook.',
@@ -289,16 +268,15 @@ const settings: TranslationStrings = {
'settings.ntfyUrl.tokenHint': 'Necessário para tópicos protegidos por senha.', 'settings.ntfyUrl.tokenHint': 'Necessário para tópicos protegidos por senha.',
'settings.ntfyUrl.saved': 'Configurações do Ntfy salvas', 'settings.ntfyUrl.saved': 'Configurações do Ntfy salvas',
'settings.ntfyUrl.test': 'Testar', 'settings.ntfyUrl.test': 'Testar',
'settings.ntfyUrl.testSuccess': 'settings.ntfyUrl.testSuccess': 'Notificação de teste do Ntfy enviada com sucesso',
'Notificação de teste do Ntfy enviada com sucesso',
'settings.ntfyUrl.testFailed': 'Falha na notificação de teste do Ntfy', 'settings.ntfyUrl.testFailed': 'Falha na notificação de teste do Ntfy',
'settings.ntfyUrl.tokenCleared': 'Token de acesso removido', 'settings.ntfyUrl.tokenCleared': 'Token de acesso removido',
'settings.notificationPreferences.inapp': 'In-App', 'settings.notificationPreferences.inapp': 'In-App',
'settings.notificationPreferences.webhook': 'Webhook', 'settings.notificationPreferences.webhook': 'Webhook',
'settings.notificationPreferences.email': 'Email', 'settings.notificationPreferences.email': 'Email',
'settings.notificationPreferences.ntfy': 'Ntfy', 'settings.notificationPreferences.ntfy': 'Ntfy',
"settings.currency": "Currency", 'settings.currency': 'Currency',
"settings.currencyHint": "All amounts in Costs are converted to and shown in this currency.", 'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
'settings.passkey.title': 'Passkeys', 'settings.passkey.title': 'Passkeys',
'settings.passkey.description': 'settings.passkey.description':
'Entre mais rápido e com proteção contra phishing usando uma passkey — sua impressão digital, rosto, PIN ou uma chave de segurança física. Sua senha continua disponível como reserva.', 'Entre mais rápido e com proteção contra phishing usando uma passkey — sua impressão digital, rosto, PIN ou uma chave de segurança física. Sua senha continua disponível como reserva.',
@@ -306,8 +284,7 @@ const settings: TranslationStrings = {
'As passkeys estão ativadas, mas ainda não foram totalmente configuradas neste servidor. Peça ao administrador para definir o domínio WebAuthn.', 'As passkeys estão ativadas, mas ainda não foram totalmente configuradas neste servidor. Peça ao administrador para definir o domínio WebAuthn.',
'settings.passkey.add': 'Adicionar uma passkey', 'settings.passkey.add': 'Adicionar uma passkey',
'settings.passkey.addTitle': 'Adicionar uma passkey', 'settings.passkey.addTitle': 'Adicionar uma passkey',
'settings.passkey.passwordPrompt': 'settings.passkey.passwordPrompt': 'Confirme sua senha atual e depois siga as instruções do seu dispositivo.',
'Confirme sua senha atual e depois siga as instruções do seu dispositivo.',
'settings.passkey.passwordRequired': 'A senha atual é obrigatória.', 'settings.passkey.passwordRequired': 'A senha atual é obrigatória.',
'settings.passkey.namePlaceholder': 'Nome (opcional, ex.: "iPhone")', 'settings.passkey.namePlaceholder': 'Nome (opcional, ex.: "iPhone")',
'settings.passkey.addedToast': 'Passkey adicionada', 'settings.passkey.addedToast': 'Passkey adicionada',
@@ -315,8 +292,7 @@ const settings: TranslationStrings = {
'settings.passkey.addError': 'Não foi possível adicionar a passkey', 'settings.passkey.addError': 'Não foi possível adicionar a passkey',
'settings.passkey.cancelled': 'Configuração da passkey cancelada', 'settings.passkey.cancelled': 'Configuração da passkey cancelada',
'settings.passkey.deleted': 'Passkey removida', 'settings.passkey.deleted': 'Passkey removida',
'settings.passkey.deleteConfirm': 'settings.passkey.deleteConfirm': 'Remover esta passkey? Confirme com sua senha.',
'Remover esta passkey? Confirme com sua senha.',
'settings.passkey.rename': 'Renomear', 'settings.passkey.rename': 'Renomear',
'settings.passkey.defaultName': 'Passkey', 'settings.passkey.defaultName': 'Passkey',
'settings.passkey.synced': 'Sincronizada', 'settings.passkey.synced': 'Sincronizada',
@@ -324,9 +300,11 @@ const settings: TranslationStrings = {
'settings.passkey.lastUsed': 'Último uso', 'settings.passkey.lastUsed': 'Último uso',
'settings.passkey.neverUsed': 'Nunca usada', 'settings.passkey.neverUsed': 'Nunca usada',
'settings.mapPoiPill': 'Explorar lugares no mapa', 'settings.mapPoiPill': 'Explorar lugares no mapa',
'settings.mapPoiPillHint': 'Mostrar uma etiqueta de categoria no mapa da viagem para encontrar restaurantes, hotéis e mais por perto a partir do OpenStreetMap.', 'settings.mapPoiPillHint':
'Mostrar uma etiqueta de categoria no mapa da viagem para encontrar restaurantes, hotéis e mais por perto a partir do OpenStreetMap.',
'settings.airtrail.title': 'AirTrail', 'settings.airtrail.title': 'AirTrail',
'settings.airtrail.hint': 'Conecte seu AirTrail auto-hospedado para importar e sincronizar voos. Crie uma chave de API no AirTrail em Configurações → Segurança.', 'settings.airtrail.hint':
'Conecte seu AirTrail auto-hospedado para importar e sincronizar voos. Crie uma chave de API no AirTrail em Configurações → Segurança.',
'settings.airtrail.url': 'URL da instância', 'settings.airtrail.url': 'URL da instância',
'settings.airtrail.apiKey': 'Chave de API', 'settings.airtrail.apiKey': 'Chave de API',
'settings.airtrail.apiKeyPlaceholder': 'Chave de API Bearer', 'settings.airtrail.apiKeyPlaceholder': 'Chave de API Bearer',
@@ -334,7 +312,8 @@ const settings: TranslationStrings = {
'settings.airtrail.allowInsecureTls': 'Permitir certificados autoassinados', 'settings.airtrail.allowInsecureTls': 'Permitir certificados autoassinados',
'settings.airtrail.allowInsecureTlsHint': 'Ative apenas para uma instância confiável na sua própria rede.', 'settings.airtrail.allowInsecureTlsHint': 'Ative apenas para uma instância confiável na sua própria rede.',
'settings.airtrail.writeBack': 'Gravar alterações de volta no AirTrail', 'settings.airtrail.writeBack': 'Gravar alterações de volta no AirTrail',
'settings.airtrail.writeBackHint': 'Desativado por padrão: o AirTrail é a fonte da verdade e o TREK apenas lê dele. Ative para enviar ao AirTrail as alterações feitas no TREK.', 'settings.airtrail.writeBackHint':
'Desativado por padrão: o AirTrail é a fonte da verdade e o TREK apenas lê dele. Ative para enviar ao AirTrail as alterações feitas no TREK.',
'settings.airtrail.connected': 'Conectado', 'settings.airtrail.connected': 'Conectado',
'settings.airtrail.notConnected': 'Não conectado', 'settings.airtrail.notConnected': 'Não conectado',
'settings.airtrail.toast.saved': 'Conexão com o AirTrail salva', 'settings.airtrail.toast.saved': 'Conexão com o AirTrail salva',
+1 -2
View File
@@ -2,8 +2,7 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = { const shared: TranslationStrings = {
'shared.expired': 'Link expirado ou inválido', 'shared.expired': 'Link expirado ou inválido',
'shared.expiredHint': 'shared.expiredHint': 'Este link de viagem compartilhado não está mais ativo.',
'Este link de viagem compartilhado não está mais ativo.',
'shared.readOnly': 'Visualização somente leitura', 'shared.readOnly': 'Visualização somente leitura',
'shared.tabPlan': 'Plano', 'shared.tabPlan': 'Plano',
'shared.tabBookings': 'Reservas', 'shared.tabBookings': 'Reservas',
+14 -28
View File
@@ -5,12 +5,9 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.body': 'system_notice.welcome_v1.body':
'Seu planejador de viagens tudo-em-um. Crie roteiros, compartilhe viagens com amigos e fique organizado — online ou offline.', 'Seu planejador de viagens tudo-em-um. Crie roteiros, compartilhe viagens com amigos e fique organizado — online ou offline.',
'system_notice.welcome_v1.cta_label': 'Planejar uma viagem', 'system_notice.welcome_v1.cta_label': 'Planejar uma viagem',
'system_notice.welcome_v1.hero_alt': 'system_notice.welcome_v1.hero_alt': 'Destino de viagem pitoresco com a interface do TREK',
'Destino de viagem pitoresco com a interface do TREK', 'system_notice.welcome_v1.highlight_plan': 'Roteiros dia a dia para qualquer viagem',
'system_notice.welcome_v1.highlight_plan': 'system_notice.welcome_v1.highlight_share': 'Colabore com seus companheiros de viagem',
'Roteiros dia a dia para qualquer viagem',
'system_notice.welcome_v1.highlight_share':
'Colabore com seus companheiros de viagem',
'system_notice.welcome_v1.highlight_offline': 'Funciona offline no celular', 'system_notice.welcome_v1.highlight_offline': 'Funciona offline no celular',
'system_notice.dev_test_modal.title': '[Dev] Test notice', 'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.', 'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
@@ -26,38 +23,27 @@ const system_notice: TranslationStrings = {
'system_notice.v3_journey.body': 'system_notice.v3_journey.body':
'Documente suas viagens como histórias ricas com cronologias, galerias de fotos e mapas interativos.', 'Documente suas viagens como histórias ricas com cronologias, galerias de fotos e mapas interativos.',
'system_notice.v3_journey.cta_label': 'Abrir Journey', 'system_notice.v3_journey.cta_label': 'Abrir Journey',
'system_notice.v3_journey.highlight_timeline': 'system_notice.v3_journey.highlight_timeline': 'Linha do tempo e galeria diária',
'Linha do tempo e galeria diária',
'system_notice.v3_journey.highlight_photos': 'Importar do Immich ou Synology', 'system_notice.v3_journey.highlight_photos': 'Importar do Immich ou Synology',
'system_notice.v3_journey.highlight_share': 'system_notice.v3_journey.highlight_share': 'Compartilhar publicamente — sem login',
'Compartilhar publicamente — sem login', 'system_notice.v3_journey.highlight_export': 'Exportar como álbum de fotos PDF',
'system_notice.v3_journey.highlight_export':
'Exportar como álbum de fotos PDF',
'system_notice.v3_features.title': 'Mais destaques na versão 3.0', 'system_notice.v3_features.title': 'Mais destaques na versão 3.0',
'system_notice.v3_features.body': 'system_notice.v3_features.body': 'Algumas outras novidades que vale a pena conhecer nesta versão.',
'Algumas outras novidades que vale a pena conhecer nesta versão.', 'system_notice.v3_features.highlight_dashboard': 'Redesign do painel mobile-first',
'system_notice.v3_features.highlight_dashboard': 'system_notice.v3_features.highlight_offline': 'Modo offline completo como PWA',
'Redesign do painel mobile-first', 'system_notice.v3_features.highlight_search': 'Autocompleção de lugares em tempo real',
'system_notice.v3_features.highlight_offline': 'system_notice.v3_features.highlight_import': 'Importar lugares de arquivos KMZ/KML',
'Modo offline completo como PWA',
'system_notice.v3_features.highlight_search':
'Autocompleção de lugares em tempo real',
'system_notice.v3_features.highlight_import':
'Importar lugares de arquivos KMZ/KML',
'system_notice.v3_mcp.title': 'MCP: atualização OAuth 2.1', 'system_notice.v3_mcp.title': 'MCP: atualização OAuth 2.1',
'system_notice.v3_mcp.body': 'system_notice.v3_mcp.body':
'A integração MCP foi completamente reformulada. OAuth 2.1 agora é o método de autenticação recomendado. Tokens estáticos (trek_…) foram descontinuados e serão removidos em uma versão futura.', 'A integração MCP foi completamente reformulada. OAuth 2.1 agora é o método de autenticação recomendado. Tokens estáticos (trek_…) foram descontinuados e serão removidos em uma versão futura.',
'system_notice.v3_mcp.highlight_oauth': 'OAuth 2.1 recomendado (mcp-remote)', 'system_notice.v3_mcp.highlight_oauth': 'OAuth 2.1 recomendado (mcp-remote)',
'system_notice.v3_mcp.highlight_scopes': '24 escopos de permissão granulares', 'system_notice.v3_mcp.highlight_scopes': '24 escopos de permissão granulares',
'system_notice.v3_mcp.highlight_deprecated': 'system_notice.v3_mcp.highlight_deprecated': 'Tokens estáticos trek_ descontinuados',
'Tokens estáticos trek_ descontinuados', 'system_notice.v3_mcp.highlight_tools': 'Conjunto de ferramentas e prompts expandido',
'system_notice.v3_mcp.highlight_tools':
'Conjunto de ferramentas e prompts expandido',
'system_notice.v3_thankyou.title': 'Uma nota pessoal minha', 'system_notice.v3_thankyou.title': 'Uma nota pessoal minha',
'system_notice.v3_thankyou.body': 'system_notice.v3_thankyou.body':
'Antes de seguir em frente — quero fazer uma pausa.\n\nO TREK começou como um projeto paralelo que criei para minhas próprias viagens. Nunca imaginei que cresceria a ponto de 4.000 de vocês confiarem nele para planejar suas aventuras. Cada estrela, cada issue, cada pedido de recurso — eu leio todos, e eles me mantêm firme nas noites longas entre um trabalho em tempo integral e a universidade.\n\nQuero que saibam: o TREK sempre será open source, sempre self-hosted, sempre de vocês. Sem rastreamento, sem assinaturas, sem pegadinhas. Apenas uma ferramenta feita por alguém que ama viajar tanto quanto vocês.\n\nAgradecimento especial ao [jubnl](https://github.com/jubnl) — você se tornou um colaborador incrível. Muito do que torna a versão 3.0 especial tem a sua marca. Obrigado por acreditar neste projeto quando ele ainda era bem cru.\n\nE a cada um de vocês que reportou um bug, traduziu uma string, compartilhou o TREK com um amigo ou simplesmente o usou para planejar uma viagem — **obrigado**. Vocês são a razão de tudo isso existir.\n\nQue venham muitas mais aventuras juntos.\n\n— Maurice\n\n---\n\n[Junte-se à comunidade no Discord](https://discord.gg/7Q6M6jDwzf)\n\nSe o TREK torna suas viagens melhores, um [cafezinho](https://ko-fi.com/mauriceboe) sempre mantém as luzes acesas.', 'Antes de seguir em frente — quero fazer uma pausa.\n\nO TREK começou como um projeto paralelo que criei para minhas próprias viagens. Nunca imaginei que cresceria a ponto de 4.000 de vocês confiarem nele para planejar suas aventuras. Cada estrela, cada issue, cada pedido de recurso — eu leio todos, e eles me mantêm firme nas noites longas entre um trabalho em tempo integral e a universidade.\n\nQuero que saibam: o TREK sempre será open source, sempre self-hosted, sempre de vocês. Sem rastreamento, sem assinaturas, sem pegadinhas. Apenas uma ferramenta feita por alguém que ama viajar tanto quanto vocês.\n\nAgradecimento especial ao [jubnl](https://github.com/jubnl) — você se tornou um colaborador incrível. Muito do que torna a versão 3.0 especial tem a sua marca. Obrigado por acreditar neste projeto quando ele ainda era bem cru.\n\nE a cada um de vocês que reportou um bug, traduziu uma string, compartilhou o TREK com um amigo ou simplesmente o usou para planejar uma viagem — **obrigado**. Vocês são a razão de tudo isso existir.\n\nQue venham muitas mais aventuras juntos.\n\n— Maurice\n\n---\n\n[Junte-se à comunidade no Discord](https://discord.gg/7Q6M6jDwzf)\n\nSe o TREK torna suas viagens melhores, um [cafezinho](https://ko-fi.com/mauriceboe) sempre mantém as luzes acesas.',
'system_notice.v3014_whitespace_collision.title': 'system_notice.v3014_whitespace_collision.title': 'Ação necessária: conflito de conta de usuário',
'Ação necessária: conflito de conta de usuário',
'system_notice.v3014_whitespace_collision.body': 'system_notice.v3014_whitespace_collision.body':
'A atualização 3.0.14 detectou um ou mais conflitos de nome de usuário ou e-mail causados por espaços em branco no início ou fim dos valores armazenados. As contas afetadas foram renomeadas automaticamente. Verifique os logs do servidor por linhas começando com **[migration] WHITESPACE COLLISION** para identificar quais contas precisam de revisão.', 'A atualização 3.0.14 detectou um ou mais conflitos de nome de usuário ou e-mail causados por espaços em branco no início ou fim dos valores armazenados. As contas afetadas foram renomeadas automaticamente. Verifique os logs do servidor por linhas começando com **[migration] WHITESPACE COLLISION** para identificar quais contas precisam de revisão.',
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ const trip: TranslationStrings = {
'trip.tabs.packingShort': 'Mala', 'trip.tabs.packingShort': 'Mala',
'trip.tabs.lists': 'Listas', 'trip.tabs.lists': 'Listas',
'trip.tabs.listsShort': 'Listas', 'trip.tabs.listsShort': 'Listas',
'trip.tabs.budget': "Costs", 'trip.tabs.budget': 'Costs',
'trip.tabs.files': 'Arquivos', 'trip.tabs.files': 'Arquivos',
'trip.loading': 'Carregando viagem...', 'trip.loading': 'Carregando viagem...',
'trip.mobilePlan': 'Plano', 'trip.mobilePlan': 'Plano',
+13 -26
View File
@@ -17,8 +17,7 @@ const vacay: TranslationStrings = {
'vacay.editPerson': 'Editar pessoa', 'vacay.editPerson': 'Editar pessoa',
'vacay.removePerson': 'Remover pessoa', 'vacay.removePerson': 'Remover pessoa',
'vacay.removePersonConfirm': 'Remover {name}?', 'vacay.removePersonConfirm': 'Remover {name}?',
'vacay.removePersonHint': 'vacay.removePersonHint': 'Todas as entradas de férias desta pessoa serão excluídas permanentemente.',
'Todas as entradas de férias desta pessoa serão excluídas permanentemente.',
'vacay.personName': 'Nome', 'vacay.personName': 'Nome',
'vacay.personNamePlaceholder': 'Digite o nome', 'vacay.personNamePlaceholder': 'Digite o nome',
'vacay.color': 'Cor', 'vacay.color': 'Cor',
@@ -43,8 +42,7 @@ const vacay: TranslationStrings = {
'vacay.fri': 'Sex', 'vacay.fri': 'Sex',
'vacay.sat': 'Sáb', 'vacay.sat': 'Sáb',
'vacay.sun': 'Dom', 'vacay.sun': 'Dom',
'vacay.blockWeekendsHint': 'vacay.blockWeekendsHint': 'Impedir entradas de férias aos sábados e domingos',
'Impedir entradas de férias aos sábados e domingos',
'vacay.publicHolidays': 'Feriados nacionais', 'vacay.publicHolidays': 'Feriados nacionais',
'vacay.publicHolidaysHint': 'Marcar feriados nacionais no calendário', 'vacay.publicHolidaysHint': 'Marcar feriados nacionais no calendário',
'vacay.selectCountry': 'Selecione o país', 'vacay.selectCountry': 'Selecione o país',
@@ -54,26 +52,20 @@ const vacay: TranslationStrings = {
'vacay.calendarColor': 'Cor', 'vacay.calendarColor': 'Cor',
'vacay.noCalendars': 'Nenhum calendário de feriados adicionado ainda', 'vacay.noCalendars': 'Nenhum calendário de feriados adicionado ainda',
'vacay.companyHolidays': 'Feriados da empresa', 'vacay.companyHolidays': 'Feriados da empresa',
'vacay.companyHolidaysHint': 'vacay.companyHolidaysHint': 'Permitir marcar dias de feriado em toda a empresa',
'Permitir marcar dias de feriado em toda a empresa', 'vacay.companyHolidaysNoDeduct': 'Feriados da empresa não contam como dias de férias.',
'vacay.companyHolidaysNoDeduct':
'Feriados da empresa não contam como dias de férias.',
'vacay.weekStart': 'Semana começa em', 'vacay.weekStart': 'Semana começa em',
'vacay.weekStartHint': 'vacay.weekStartHint': 'Escolha se a semana começa na segunda-feira ou no domingo',
'Escolha se a semana começa na segunda-feira ou no domingo',
'vacay.carryOver': 'Acúmulo', 'vacay.carryOver': 'Acúmulo',
'vacay.carryOverHint': 'vacay.carryOverHint': 'Levar automaticamente os dias de férias restantes para o ano seguinte',
'Levar automaticamente os dias de férias restantes para o ano seguinte',
'vacay.sharing': 'Compartilhamento', 'vacay.sharing': 'Compartilhamento',
'vacay.sharingHint': 'vacay.sharingHint': 'Compartilhe seu plano de férias com outros usuários do TREK',
'Compartilhe seu plano de férias com outros usuários do TREK',
'vacay.owner': 'Proprietário', 'vacay.owner': 'Proprietário',
'vacay.shareEmailPlaceholder': 'E-mail do usuário TREK', 'vacay.shareEmailPlaceholder': 'E-mail do usuário TREK',
'vacay.shareSuccess': 'Plano compartilhado com sucesso', 'vacay.shareSuccess': 'Plano compartilhado com sucesso',
'vacay.shareError': 'Não foi possível compartilhar o plano', 'vacay.shareError': 'Não foi possível compartilhar o plano',
'vacay.dissolve': 'Encerrar fusão', 'vacay.dissolve': 'Encerrar fusão',
'vacay.dissolveHint': 'vacay.dissolveHint': 'Separar os calendários novamente. Suas entradas serão mantidas.',
'Separar os calendários novamente. Suas entradas serão mantidas.',
'vacay.dissolveAction': 'Encerrar', 'vacay.dissolveAction': 'Encerrar',
'vacay.dissolved': 'Calendário separado', 'vacay.dissolved': 'Calendário separado',
'vacay.fusedWith': 'Fundido com', 'vacay.fusedWith': 'Fundido com',
@@ -81,8 +73,7 @@ const vacay: TranslationStrings = {
'vacay.noData': 'Sem dados', 'vacay.noData': 'Sem dados',
'vacay.changeColor': 'Alterar cor', 'vacay.changeColor': 'Alterar cor',
'vacay.inviteUser': 'Convidar usuário', 'vacay.inviteUser': 'Convidar usuário',
'vacay.inviteHint': 'vacay.inviteHint': 'Convide outro usuário TREK para compartilhar um calendário de férias combinado.',
'Convide outro usuário TREK para compartilhar um calendário de férias combinado.',
'vacay.selectUser': 'Selecionar usuário', 'vacay.selectUser': 'Selecionar usuário',
'vacay.sendInvite': 'Enviar convite', 'vacay.sendInvite': 'Enviar convite',
'vacay.inviteSent': 'Convite enviado', 'vacay.inviteSent': 'Convite enviado',
@@ -93,15 +84,11 @@ const vacay: TranslationStrings = {
'vacay.decline': 'Recusar', 'vacay.decline': 'Recusar',
'vacay.acceptFusion': 'Aceitar e fundir', 'vacay.acceptFusion': 'Aceitar e fundir',
'vacay.inviteTitle': 'Pedido de fusão', 'vacay.inviteTitle': 'Pedido de fusão',
'vacay.inviteWantsToFuse': 'vacay.inviteWantsToFuse': 'quer compartilhar um calendário de férias com você.',
'quer compartilhar um calendário de férias com você.', 'vacay.fuseInfo1': 'Ambos verão todas as entradas de férias em um calendário compartilhado.',
'vacay.fuseInfo1':
'Ambos verão todas as entradas de férias em um calendário compartilhado.',
'vacay.fuseInfo2': 'Ambos podem criar e editar entradas um do outro.', 'vacay.fuseInfo2': 'Ambos podem criar e editar entradas um do outro.',
'vacay.fuseInfo3': 'vacay.fuseInfo3': 'Ambos podem excluir entradas e alterar direitos de férias.',
'Ambos podem excluir entradas e alterar direitos de férias.', 'vacay.fuseInfo4': 'Configurações como feriados nacionais e da empresa são compartilhadas.',
'vacay.fuseInfo4':
'Configurações como feriados nacionais e da empresa são compartilhadas.',
'vacay.fuseInfo5': 'vacay.fuseInfo5':
'A fusão pode ser encerrada a qualquer momento por qualquer parte. Suas entradas serão preservadas.', 'A fusão pode ser encerrada a qualquer momento por qualquer parte. Suas entradas serão preservadas.',
}; };
+49 -92
View File
@@ -2,8 +2,7 @@ import type { TranslationStrings } from '../types';
const admin: TranslationStrings = { const admin: TranslationStrings = {
'admin.notifications.title': 'Oznámení', 'admin.notifications.title': 'Oznámení',
'admin.notifications.hint': 'admin.notifications.hint': 'Vyberte kanál oznámení. Současně může být aktivní pouze jeden.',
'Vyberte kanál oznámení. Současně může být aktivní pouze jeden.',
'admin.notifications.none': 'Vypnuto', 'admin.notifications.none': 'Vypnuto',
'admin.notifications.email': 'E-mail (SMTP)', 'admin.notifications.email': 'E-mail (SMTP)',
'admin.notifications.webhook': 'Webhook', 'admin.notifications.webhook': 'Webhook',
@@ -11,13 +10,11 @@ const admin: TranslationStrings = {
'admin.notifications.saved': 'Nastavení oznámení uloženo', 'admin.notifications.saved': 'Nastavení oznámení uloženo',
'admin.notifications.testWebhook': 'Odeslat testovací webhook', 'admin.notifications.testWebhook': 'Odeslat testovací webhook',
'admin.notifications.testWebhookSuccess': 'Testovací webhook úspěšně odeslán', 'admin.notifications.testWebhookSuccess': 'Testovací webhook úspěšně odeslán',
'admin.notifications.testWebhookFailed': 'admin.notifications.testWebhookFailed': 'Odeslání testovacího webhooku se nezdařilo',
'Odeslání testovacího webhooku se nezdařilo',
'admin.smtp.title': 'E-mail a oznámení', 'admin.smtp.title': 'E-mail a oznámení',
'admin.smtp.hint': 'Konfigurace SMTP pro odesílání e-mailových oznámení.', 'admin.smtp.hint': 'Konfigurace SMTP pro odesílání e-mailových oznámení.',
'admin.smtp.testButton': 'Odeslat testovací e-mail', 'admin.smtp.testButton': 'Odeslat testovací e-mail',
'admin.webhook.hint': 'admin.webhook.hint': 'Odesílat oznámení na externí webhook (Discord, Slack atd.).',
'Odesílat oznámení na externí webhook (Discord, Slack atd.).',
'admin.smtp.testSuccess': 'Testovací e-mail byl úspěšně odeslán', 'admin.smtp.testSuccess': 'Testovací e-mail byl úspěšně odeslán',
'admin.smtp.testFailed': 'Odeslání testovacího e-mailu se nezdařilo', 'admin.smtp.testFailed': 'Odeslání testovacího e-mailu se nezdařilo',
'admin.title': 'Administrace', 'admin.title': 'Administrace',
@@ -40,8 +37,7 @@ const admin: TranslationStrings = {
'admin.editUser': 'Upravit uživatele', 'admin.editUser': 'Upravit uživatele',
'admin.newPassword': 'Nové heslo', 'admin.newPassword': 'Nové heslo',
'admin.newPasswordHint': 'Ponechte prázdné pro zachování současného hesla', 'admin.newPasswordHint': 'Ponechte prázdné pro zachování současného hesla',
'admin.deleteUser': 'admin.deleteUser': 'Smazat uživatele „{name}“? Všechny jeho cesty budou trvale smazány.',
'Smazat uživatele „{name}“? Všechny jeho cesty budou trvale smazány.',
'admin.deleteUserTitle': 'Smazat uživatele', 'admin.deleteUserTitle': 'Smazat uživatele',
'admin.newPasswordPlaceholder': 'Zadejte nové heslo…', 'admin.newPasswordPlaceholder': 'Zadejte nové heslo…',
'admin.toast.loadError': 'Nepodařilo se načíst data administrace', 'admin.toast.loadError': 'Nepodařilo se načíst data administrace',
@@ -52,8 +48,7 @@ const admin: TranslationStrings = {
'admin.toast.cannotDeleteSelf': 'Nemůžete smazat svůj vlastní účet', 'admin.toast.cannotDeleteSelf': 'Nemůžete smazat svůj vlastní účet',
'admin.toast.userCreated': 'Uživatel byl vytvořen', 'admin.toast.userCreated': 'Uživatel byl vytvořen',
'admin.toast.createError': 'Nepodařilo se vytvořit uživatele', 'admin.toast.createError': 'Nepodařilo se vytvořit uživatele',
'admin.toast.fieldsRequired': 'admin.toast.fieldsRequired': 'Uživatelské jméno, e-mail a heslo jsou povinné',
'Uživatelské jméno, e-mail a heslo jsou povinné',
'admin.createUser': 'Vytvořit uživatele', 'admin.createUser': 'Vytvořit uživatele',
'admin.invite.title': 'Pozvánky', 'admin.invite.title': 'Pozvánky',
'admin.invite.subtitle': 'Vytvářejte jednorázové registrační odkazy', 'admin.invite.subtitle': 'Vytvářejte jednorázové registrační odkazy',
@@ -80,25 +75,20 @@ const admin: TranslationStrings = {
'admin.passwordLogin': 'Password Login', 'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password', 'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration', 'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login', 'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO', 'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning', 'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'admin.envOverrideHint':
'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.', 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled', 'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Vyžadovat dvoufázové ověření (2FA)', 'admin.requireMfa': 'Vyžadovat dvoufázové ověření (2FA)',
'admin.requireMfaHint': 'admin.requireMfaHint': 'Uživatelé bez 2FA musí dokončit nastavení v Nastavení před použitím aplikace.',
'Uživatelé bez 2FA musí dokončit nastavení v Nastavení před použitím aplikace.',
'admin.apiKeys': 'API klíče', 'admin.apiKeys': 'API klíče',
'admin.apiKeysHint': 'admin.apiKeysHint': 'Volitelné. Povoluje rozšířená data o místech (fotky, počasí).',
'Volitelné. Povoluje rozšířená data o místech (fotky, počasí).',
'admin.mapsKey': 'Google Maps API klíč', 'admin.mapsKey': 'Google Maps API klíč',
'admin.mapsKeyHint': 'admin.mapsKeyHint': 'Povinné pro hledání míst. Získáte na console.cloud.google.com',
'Povinné pro hledání míst. Získáte na console.cloud.google.com',
'admin.mapsKeyHintLong': 'admin.mapsKeyHintLong':
'Bez API klíče se pro hledání používá OpenStreetMap. S Google klíčem lze načítat fotky, hodnocení a otevírací dobu.', 'Bez API klíče se pro hledání používá OpenStreetMap. S Google klíčem lze načítat fotky, hodnocení a otevírací dobu.',
'admin.recommended': 'Doporučeno', 'admin.recommended': 'Doporučeno',
@@ -109,21 +99,17 @@ const admin: TranslationStrings = {
'admin.keyInvalid': 'Neplatný', 'admin.keyInvalid': 'Neplatný',
'admin.keySaved': 'API klíče byly uloženy', 'admin.keySaved': 'API klíče byly uloženy',
'admin.oidcTitle': 'Jednotné přihlášení (OIDC)', 'admin.oidcTitle': 'Jednotné přihlášení (OIDC)',
'admin.oidcSubtitle': 'admin.oidcSubtitle': 'Povolit přihlášení přes externí poskytovatele (Google, Apple, Authentik, Keycloak).',
'Povolit přihlášení přes externí poskytovatele (Google, Apple, Authentik, Keycloak).',
'admin.oidcDisplayName': 'Zobrazované jméno', 'admin.oidcDisplayName': 'Zobrazované jméno',
'admin.oidcIssuer': 'URL vydavatele (Issuer)', 'admin.oidcIssuer': 'URL vydavatele (Issuer)',
'admin.oidcIssuerHint': 'admin.oidcIssuerHint': 'OpenID Connect Issuer URL, např. https://accounts.google.com',
'OpenID Connect Issuer URL, např. https://accounts.google.com',
'admin.oidcSaved': 'Konfigurace OIDC uložena', 'admin.oidcSaved': 'Konfigurace OIDC uložena',
'admin.oidcOnlyMode': 'Zakázat ověřování heslem', 'admin.oidcOnlyMode': 'Zakázat ověřování heslem',
'admin.oidcOnlyModeHint': 'admin.oidcOnlyModeHint':
'Pokud je zapnuto, je povolen pouze SSO login. Registrace i přihlášení heslem budou zablokovány.', 'Pokud je zapnuto, je povolen pouze SSO login. Registrace i přihlášení heslem budou zablokovány.',
'admin.fileTypes': 'Povolené typy souborů', 'admin.fileTypes': 'Povolené typy souborů',
'admin.fileTypesHint': 'admin.fileTypesHint': 'Nastavte, které typy souborů mohou uživatelé nahrávat.',
'Nastavte, které typy souborů mohou uživatelé nahrávat.', 'admin.fileTypesFormat': 'Přípony oddělené čárkou (např. jpg,png,pdf,doc). Použijte * pro všechny typy.',
'admin.fileTypesFormat':
'Přípony oddělené čárkou (např. jpg,png,pdf,doc). Použijte * pro všechny typy.',
'admin.fileTypesSaved': 'Nastavení souborů uloženo', 'admin.fileTypesSaved': 'Nastavení souborů uloženo',
'admin.placesPhotos.title': 'Fotografie míst', 'admin.placesPhotos.title': 'Fotografie míst',
'admin.placesPhotos.subtitle': 'admin.placesPhotos.subtitle':
@@ -135,8 +121,7 @@ const admin: TranslationStrings = {
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Načítání podrobných informací o místě (hodiny, hodnocení, web) z Google Places API. Zakázáním ušetříte kvótu API.', 'Načítání podrobných informací o místě (hodiny, hodnocení, web) z Google Places API. Zakázáním ušetříte kvótu API.',
'admin.bagTracking.title': 'Sledování zavazadel', 'admin.bagTracking.title': 'Sledování zavazadel',
'admin.bagTracking.subtitle': 'admin.bagTracking.subtitle': 'Povolit váhu a přiřazení k zavazadlům u položek balení',
'Povolit váhu a přiřazení k zavazadlům u položek balení',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
'admin.collab.chat.subtitle': 'Zasílání zpráv v reálném čase', 'admin.collab.chat.subtitle': 'Zasílání zpráv v reálném čase',
'admin.collab.notes.title': 'Poznámky', 'admin.collab.notes.title': 'Poznámky',
@@ -155,11 +140,9 @@ const admin: TranslationStrings = {
'admin.defaultSettings.resetToBuiltIn': 'obnovit', 'admin.defaultSettings.resetToBuiltIn': 'obnovit',
'admin.tabs.templates': 'Šablony seznamů', 'admin.tabs.templates': 'Šablony seznamů',
'admin.packingTemplates.title': 'Šablony pro balení', 'admin.packingTemplates.title': 'Šablony pro balení',
'admin.packingTemplates.subtitle': 'admin.packingTemplates.subtitle': 'Vytvářejte opakovaně použitelné seznamy pro své cesty',
'Vytvářejte opakovaně použitelné seznamy pro své cesty',
'admin.packingTemplates.create': 'Nová šablona', 'admin.packingTemplates.create': 'Nová šablona',
'admin.packingTemplates.namePlaceholder': 'admin.packingTemplates.namePlaceholder': 'Název šablony (např. Dovolená u moře)',
'Název šablony (např. Dovolená u moře)',
'admin.packingTemplates.empty': 'Zatím nejsou vytvořeny žádné šablony', 'admin.packingTemplates.empty': 'Zatím nejsou vytvořeny žádné šablony',
'admin.packingTemplates.items': 'položek', 'admin.packingTemplates.items': 'položek',
'admin.packingTemplates.categories': 'kategorií', 'admin.packingTemplates.categories': 'kategorií',
@@ -177,26 +160,19 @@ const admin: TranslationStrings = {
'admin.addons.title': 'Doplňky', 'admin.addons.title': 'Doplňky',
'admin.addons.subtitle': 'Zapněte nebo vypněte funkce a přizpůsobte si TREK.', 'admin.addons.subtitle': 'Zapněte nebo vypněte funkce a přizpůsobte si TREK.',
'admin.addons.catalog.memories.name': 'Fotky (Immich)', 'admin.addons.catalog.memories.name': 'Fotky (Immich)',
'admin.addons.catalog.memories.description': 'admin.addons.catalog.memories.description': 'Sdílejte cestovní fotky přes vaši instanci Immich',
'Sdílejte cestovní fotky přes vaši instanci Immich',
'admin.addons.catalog.packing.name': 'Seznamy', 'admin.addons.catalog.packing.name': 'Seznamy',
'admin.addons.catalog.packing.description': 'admin.addons.catalog.packing.description': 'Balicí seznamy a úkoly pro vaše výlety',
'Balicí seznamy a úkoly pro vaše výlety',
'admin.addons.catalog.budget.name': 'Rozpočet', 'admin.addons.catalog.budget.name': 'Rozpočet',
'admin.addons.catalog.budget.description': 'admin.addons.catalog.budget.description': 'Sledování výdajů a plánování rozpočtu cesty',
'Sledování výdajů a plánování rozpočtu cesty',
'admin.addons.catalog.documents.name': 'Dokumenty', 'admin.addons.catalog.documents.name': 'Dokumenty',
'admin.addons.catalog.documents.description': 'admin.addons.catalog.documents.description': 'Ukládání a správa cestovních dokladů',
'Ukládání a správa cestovních dokladů',
'admin.addons.catalog.vacay.name': 'Dovolená (Vacay)', 'admin.addons.catalog.vacay.name': 'Dovolená (Vacay)',
'admin.addons.catalog.vacay.description': 'admin.addons.catalog.vacay.description': 'Osobní plánovač dovolené s kalendářem',
'Osobní plánovač dovolené s kalendářem',
'admin.addons.catalog.atlas.name': 'Atlas', 'admin.addons.catalog.atlas.name': 'Atlas',
'admin.addons.catalog.atlas.description': 'admin.addons.catalog.atlas.description': 'Mapa světa s navštívenými zeměmi a statistikami',
'Mapa světa s navštívenými zeměmi a statistikami',
'admin.addons.catalog.collab.name': 'Spolupráce', 'admin.addons.catalog.collab.name': 'Spolupráce',
'admin.addons.catalog.collab.description': 'admin.addons.catalog.collab.description': 'Poznámky v reálném čase, hlasování a chat pro plánování',
'Poznámky v reálném čase, hlasování a chat pro plánování',
'admin.addons.enabled': 'Povoleno', 'admin.addons.enabled': 'Povoleno',
'admin.addons.disabled': 'Zakázáno', 'admin.addons.disabled': 'Zakázáno',
'admin.addons.type.trip': 'Cesta', 'admin.addons.type.trip': 'Cesta',
@@ -204,20 +180,16 @@ const admin: TranslationStrings = {
'admin.addons.type.integration': 'Integrace', 'admin.addons.type.integration': 'Integrace',
'admin.addons.tripHint': 'Dostupné jako karta v rámci každé cesty', 'admin.addons.tripHint': 'Dostupné jako karta v rámci každé cesty',
'admin.addons.globalHint': 'Dostupné jako samostatná sekce v hlavní navigaci', 'admin.addons.globalHint': 'Dostupné jako samostatná sekce v hlavní navigaci',
'admin.addons.integrationHint': 'admin.addons.integrationHint': 'Backendové služby a API integrace bez vlastní stránky',
'Backendové služby a API integrace bez vlastní stránky',
'admin.addons.toast.updated': 'Doplněk byl aktualizován', 'admin.addons.toast.updated': 'Doplněk byl aktualizován',
'admin.addons.toast.error': 'Aktualizace doplňku se nezdařila', 'admin.addons.toast.error': 'Aktualizace doplňku se nezdařila',
'admin.addons.noAddons': 'Žádné doplňky nejsou k dispozici', 'admin.addons.noAddons': 'Žádné doplňky nejsou k dispozici',
'admin.addons.catalog.mcp.name': 'MCP', 'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'admin.addons.catalog.mcp.description': 'Model Context Protocol pro integraci AI asistentů',
'Model Context Protocol pro integraci AI asistentů', 'admin.addons.subtitleBefore': 'Zapněte nebo vypněte funkce a přizpůsobte si ',
'admin.addons.subtitleBefore':
'Zapněte nebo vypněte funkce a přizpůsobte si ',
'admin.addons.subtitleAfter': '.', 'admin.addons.subtitleAfter': '.',
'admin.tabs.audit': 'Audit', 'admin.tabs.audit': 'Audit',
'admin.audit.subtitle': 'admin.audit.subtitle': 'Bezpečnostní a administrátorské události (zálohy, uživatelé, 2FA, nastavení).',
'Bezpečnostní a administrátorské události (zálohy, uživatelé, 2FA, nastavení).',
'admin.audit.empty': 'Zatím žádné záznamy auditu.', 'admin.audit.empty': 'Zatím žádné záznamy auditu.',
'admin.audit.refresh': 'Obnovit', 'admin.audit.refresh': 'Obnovit',
'admin.audit.loadMore': 'Načíst další', 'admin.audit.loadMore': 'Načíst další',
@@ -230,8 +202,7 @@ const admin: TranslationStrings = {
'admin.audit.col.details': 'Detaily', 'admin.audit.col.details': 'Detaily',
'admin.tabs.mcpTokens': 'MCP přístup', 'admin.tabs.mcpTokens': 'MCP přístup',
'admin.mcpTokens.title': 'MCP přístup', 'admin.mcpTokens.title': 'MCP přístup',
'admin.mcpTokens.subtitle': 'admin.mcpTokens.subtitle': 'Správa OAuth relací a API tokenů všech uživatelů',
'Správa OAuth relací a API tokenů všech uživatelů',
'admin.mcpTokens.sectionTitle': 'API tokeny', 'admin.mcpTokens.sectionTitle': 'API tokeny',
'admin.mcpTokens.owner': 'Vlastník', 'admin.mcpTokens.owner': 'Vlastník',
'admin.mcpTokens.tokenName': 'Název tokenu', 'admin.mcpTokens.tokenName': 'Název tokenu',
@@ -252,8 +223,7 @@ const admin: TranslationStrings = {
'admin.oauthSessions.created': 'Vytvořeno', 'admin.oauthSessions.created': 'Vytvořeno',
'admin.oauthSessions.empty': 'Žádné aktivní OAuth relace', 'admin.oauthSessions.empty': 'Žádné aktivní OAuth relace',
'admin.oauthSessions.revokeTitle': 'Zrušit relaci', 'admin.oauthSessions.revokeTitle': 'Zrušit relaci',
'admin.oauthSessions.revokeMessage': 'admin.oauthSessions.revokeMessage': 'Tato OAuth relace bude okamžitě zrušena. Klient ztratí přístup k MCP.',
'Tato OAuth relace bude okamžitě zrušena. Klient ztratí přístup k MCP.',
'admin.oauthSessions.revokeSuccess': 'Relace zrušena', 'admin.oauthSessions.revokeSuccess': 'Relace zrušena',
'admin.oauthSessions.revokeError': 'Nepodařilo se zrušit relaci', 'admin.oauthSessions.revokeError': 'Nepodařilo se zrušit relaci',
'admin.oauthSessions.loadError': 'Nepodařilo se načíst OAuth relace', 'admin.oauthSessions.loadError': 'Nepodařilo se načíst OAuth relace',
@@ -276,27 +246,22 @@ const admin: TranslationStrings = {
'admin.weather.forecast': 'Předpověď na 16 dní', 'admin.weather.forecast': 'Předpověď na 16 dní',
'admin.weather.forecastDesc': 'Dříve 5 dní (OpenWeatherMap)', 'admin.weather.forecastDesc': 'Dříve 5 dní (OpenWeatherMap)',
'admin.weather.climate': 'Historická klimatická data', 'admin.weather.climate': 'Historická klimatická data',
'admin.weather.climateDesc': 'admin.weather.climateDesc': 'Průměry za posledních 85 let pro dny mimo 16denní předpověď',
'Průměry za posledních 85 let pro dny mimo 16denní předpověď',
'admin.weather.requests': '10 000 požadavků denně', 'admin.weather.requests': '10 000 požadavků denně',
'admin.weather.requestsDesc': 'Zdarma, bez nutnosti klíče', 'admin.weather.requestsDesc': 'Zdarma, bez nutnosti klíče',
'admin.weather.locationHint': 'admin.weather.locationHint': 'Počasí se určuje podle prvního místa se souřadnicemi v daném dni.',
'Počasí se určuje podle prvního místa se souřadnicemi v daném dni.',
'admin.update.available': 'Dostupná aktualizace', 'admin.update.available': 'Dostupná aktualizace',
'admin.update.text': 'admin.update.text': 'TREK {version} je k dispozici. Aktuálně používáte verzi {current}.',
'TREK {version} je k dispozici. Aktuálně používáte verzi {current}.',
'admin.update.button': 'Zobrazit na GitHubu', 'admin.update.button': 'Zobrazit na GitHubu',
'admin.update.install': 'Instalovat aktualizaci', 'admin.update.install': 'Instalovat aktualizaci',
'admin.update.confirmTitle': 'Instalovat aktualizaci?', 'admin.update.confirmTitle': 'Instalovat aktualizaci?',
'admin.update.confirmText': 'admin.update.confirmText':
'TREK bude aktualizován z verze {current} na {version}. Server se poté automaticky restartuje.', 'TREK bude aktualizován z verze {current} na {version}. Server se poté automaticky restartuje.',
'admin.update.dataInfo': 'admin.update.dataInfo': 'Všechna vaše data (cesty, uživatelé, API klíče, soubory) budou zachována.',
'Všechna vaše data (cesty, uživatelé, API klíče, soubory) budou zachována.',
'admin.update.warning': 'Aplikace bude během restartu krátce nedostupná.', 'admin.update.warning': 'Aplikace bude během restartu krátce nedostupná.',
'admin.update.confirm': 'Aktualizovat nyní', 'admin.update.confirm': 'Aktualizovat nyní',
'admin.update.installing': 'Aktualizace probíhá…', 'admin.update.installing': 'Aktualizace probíhá…',
'admin.update.success': 'admin.update.success': 'Aktualizace byla nainstalována! Server se restartuje…',
'Aktualizace byla nainstalována! Server se restartuje…',
'admin.update.failed': 'Aktualizace se nezdařila', 'admin.update.failed': 'Aktualizace se nezdařila',
'admin.update.backupHint': 'Před aktualizací doporučujeme vytvořit zálohu.', 'admin.update.backupHint': 'Před aktualizací doporučujeme vytvořit zálohu.',
'admin.update.backupLink': 'Přejít na zálohování', 'admin.update.backupLink': 'Přejít na zálohování',
@@ -308,18 +273,14 @@ const admin: TranslationStrings = {
'admin.notifications.emailPanel.title': 'Email (SMTP)', 'admin.notifications.emailPanel.title': 'Email (SMTP)',
'admin.notifications.webhookPanel.title': 'Webhook', 'admin.notifications.webhookPanel.title': 'Webhook',
'admin.notifications.inappPanel.title': 'In-App', 'admin.notifications.inappPanel.title': 'In-App',
'admin.notifications.inappPanel.hint': 'admin.notifications.inappPanel.hint': 'In-app oznámení jsou vždy aktivní a nelze je globálně vypnout.',
'In-app oznámení jsou vždy aktivní a nelze je globálně vypnout.',
'admin.notifications.adminWebhookPanel.title': 'Admin webhook', 'admin.notifications.adminWebhookPanel.title': 'Admin webhook',
'admin.notifications.adminWebhookPanel.hint': 'admin.notifications.adminWebhookPanel.hint':
'Tento webhook se používá výhradně pro admin oznámení (např. upozornění na verze). Je nezávislý na uživatelských webhooků a odesílá automaticky, pokud je nastavena URL.', 'Tento webhook se používá výhradně pro admin oznámení (např. upozornění na verze). Je nezávislý na uživatelských webhooků a odesílá automaticky, pokud je nastavena URL.',
'admin.notifications.adminWebhookPanel.saved': 'URL admin webhooku uložena', 'admin.notifications.adminWebhookPanel.saved': 'URL admin webhooku uložena',
'admin.notifications.adminWebhookPanel.testSuccess': 'admin.notifications.adminWebhookPanel.testSuccess': 'Testovací webhook byl úspěšně odeslán',
'Testovací webhook byl úspěšně odeslán', 'admin.notifications.adminWebhookPanel.testFailed': 'Testovací webhook selhal',
'admin.notifications.adminWebhookPanel.testFailed': 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin webhook odesílá automaticky, pokud je nastavena URL',
'Testovací webhook selhal',
'admin.notifications.adminWebhookPanel.alwaysOnHint':
'Admin webhook odesílá automaticky, pokud je nastavena URL',
'admin.notifications.ntfy': 'Ntfy', 'admin.notifications.ntfy': 'Ntfy',
'admin.ntfy.hint': 'admin.ntfy.hint':
'Umožňuje uživatelům nakonfigurovat vlastní témata ntfy pro přijímání push notifikací. Níže nastavte výchozí server pro předvyplnění nastavení uživatelů.', 'Umožňuje uživatelům nakonfigurovat vlastní témata ntfy pro přijímání push notifikací. Níže nastavte výchozí server pro předvyplnění nastavení uživatelů.',
@@ -335,29 +296,23 @@ const admin: TranslationStrings = {
'admin.notifications.adminNtfyPanel.serverPlaceholder': 'https://ntfy.sh', 'admin.notifications.adminNtfyPanel.serverPlaceholder': 'https://ntfy.sh',
'admin.notifications.adminNtfyPanel.topicLabel': 'Admin téma', 'admin.notifications.adminNtfyPanel.topicLabel': 'Admin téma',
'admin.notifications.adminNtfyPanel.topicPlaceholder': 'trek-admin-alerts', 'admin.notifications.adminNtfyPanel.topicPlaceholder': 'trek-admin-alerts',
'admin.notifications.adminNtfyPanel.tokenLabel': 'admin.notifications.adminNtfyPanel.tokenLabel': 'Přístupový token (volitelné)',
'Přístupový token (volitelné)', 'admin.notifications.adminNtfyPanel.tokenCleared': 'Přístupový token admina byl vymazán',
'admin.notifications.adminNtfyPanel.tokenCleared':
'Přístupový token admina byl vymazán',
'admin.notifications.adminNtfyPanel.saved': 'Nastavení admin Ntfy uloženo', 'admin.notifications.adminNtfyPanel.saved': 'Nastavení admin Ntfy uloženo',
'admin.notifications.adminNtfyPanel.test': 'Odeslat testovací Ntfy', 'admin.notifications.adminNtfyPanel.test': 'Odeslat testovací Ntfy',
'admin.notifications.adminNtfyPanel.testSuccess': 'admin.notifications.adminNtfyPanel.testSuccess': 'Testovací Ntfy bylo úspěšně odesláno',
'Testovací Ntfy bylo úspěšně odesláno',
'admin.notifications.adminNtfyPanel.testFailed': 'Testovací Ntfy selhalo', 'admin.notifications.adminNtfyPanel.testFailed': 'Testovací Ntfy selhalo',
'admin.notifications.adminNtfyPanel.alwaysOnHint': 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin Ntfy odesílá vždy, když je nakonfigurováno téma',
'Admin Ntfy odesílá vždy, když je nakonfigurováno téma',
'admin.notifications.adminNotificationsHint': 'admin.notifications.adminNotificationsHint':
'Nastavte, které kanály doručují admin oznámení (např. upozornění na verze). Webhook odesílá automaticky, pokud je nastavena URL admin webhooku.', 'Nastavte, které kanály doručují admin oznámení (např. upozornění na verze). Webhook odesílá automaticky, pokud je nastavena URL admin webhooku.',
'admin.notifications.tripReminders.title': 'Připomínky výletů', 'admin.notifications.tripReminders.title': 'Připomínky výletů',
'admin.notifications.tripReminders.hint': 'admin.notifications.tripReminders.hint':
'Odešle upozornění před začátkem výletu (vyžaduje nastavené dny připomínky na výletu).', 'Odešle upozornění před začátkem výletu (vyžaduje nastavené dny připomínky na výletu).',
'admin.notifications.tripReminders.enabled': 'Připomínky výletů aktivovány', 'admin.notifications.tripReminders.enabled': 'Připomínky výletů aktivovány',
'admin.notifications.tripReminders.disabled': 'admin.notifications.tripReminders.disabled': 'Připomínky výletů deaktivovány',
'Připomínky výletů deaktivovány',
'admin.tabs.notifications': 'Oznámení', 'admin.tabs.notifications': 'Oznámení',
'admin.addons.catalog.journey.name': 'Cestovní deník', 'admin.addons.catalog.journey.name': 'Cestovní deník',
'admin.addons.catalog.journey.description': 'admin.addons.catalog.journey.description': 'Sledování cest a cestovní deník s odbaveními, fotkami a denními příběhy',
'Sledování cest a cestovní deník s odbaveními, fotkami a denními příběhy',
'admin.passkey.title': 'Přihlášení přístupovým klíčem', 'admin.passkey.title': 'Přihlášení přístupovým klíčem',
'admin.passkey.cardHint': 'admin.passkey.cardHint':
'Umožněte uživatelům přihlašovat se pomocí přístupových klíčů (WebAuthn). Ve výchozím nastavení vypnuto.', 'Umožněte uživatelům přihlašovat se pomocí přístupových klíčů (WebAuthn). Ve výchozím nastavení vypnuto.',
@@ -378,11 +333,13 @@ const admin: TranslationStrings = {
'admin.passkey.resetConfirm': 'Odebrat všechny přístupové klíče uživatele {name}?', 'admin.passkey.resetConfirm': 'Odebrat všechny přístupové klíče uživatele {name}?',
'admin.passkey.resetDone': 'Odebráno {count} přístupových klíčů', 'admin.passkey.resetDone': 'Odebráno {count} přístupových klíčů',
'admin.defaultSettings.mapProvider': 'Mapový engine', 'admin.defaultSettings.mapProvider': 'Mapový engine',
'admin.defaultSettings.mapProviderHint': 'Výchozí mapa pro všechny uživatele na této instanci. Každý uživatel ji může i nadále změnit ve svém vlastním nastavení.', 'admin.defaultSettings.mapProviderHint':
'Výchozí mapa pro všechny uživatele na této instanci. Každý uživatel ji může i nadále změnit ve svém vlastním nastavení.',
'admin.defaultSettings.providerLeaflet': 'Standardní (zdarma)', 'admin.defaultSettings.providerLeaflet': 'Standardní (zdarma)',
'admin.defaultSettings.providerMapbox': 'Mapbox (3D)', 'admin.defaultSettings.providerMapbox': 'Mapbox (3D)',
'admin.defaultSettings.mapboxToken': 'Sdílený token Mapbox', 'admin.defaultSettings.mapboxToken': 'Sdílený token Mapbox',
'admin.defaultSettings.mapboxTokenHint': 'Použije se pro každého uživatele, který nezadal vlastní token — takže celá instance získá Mapbox, aniž byste klíč sdíleli s každým zvlášť. Ukládá se šifrovaně.', 'admin.defaultSettings.mapboxTokenHint':
'Použije se pro každého uživatele, který nezadal vlastní token — takže celá instance získá Mapbox, aniž byste klíč sdíleli s každým zvlášť. Ukládá se šifrovaně.',
'admin.defaultSettings.mapboxStyle': 'Styl mapy', 'admin.defaultSettings.mapboxStyle': 'Styl mapy',
'admin.defaultSettings.mapboxStylePlaceholder': 'Vyberte styl…', 'admin.defaultSettings.mapboxStylePlaceholder': 'Vyberte styl…',
'admin.defaultSettings.mapbox3d': '3D budovy & terén', 'admin.defaultSettings.mapbox3d': '3D budovy & terén',
+1 -2
View File
@@ -28,8 +28,7 @@ const atlas: TranslationStrings = {
'atlas.visitedCountries': 'Navštívené země', 'atlas.visitedCountries': 'Navštívené země',
'atlas.cities': 'Města', 'atlas.cities': 'Města',
'atlas.noData': 'Zatím žádná cestovatelská data', 'atlas.noData': 'Zatím žádná cestovatelská data',
'atlas.noDataHint': 'atlas.noDataHint': 'Vytvořte cestu a přidejte místa, abyste viděli svou mapu světa',
'Vytvořte cestu a přidejte místa, abyste viděli svou mapu světa',
'atlas.lastTrip': 'Poslední cesta', 'atlas.lastTrip': 'Poslední cesta',
'atlas.nextTrip': 'Další cesta', 'atlas.nextTrip': 'Další cesta',
'atlas.daysLeft': 'dní zbývá', 'atlas.daysLeft': 'dní zbývá',
+4 -8
View File
@@ -12,10 +12,8 @@ const backup: TranslationStrings = {
'backup.createFirst': 'Vytvořit první zálohu', 'backup.createFirst': 'Vytvořit první zálohu',
'backup.download': 'Stáhnout', 'backup.download': 'Stáhnout',
'backup.restore': 'Obnovit', 'backup.restore': 'Obnovit',
'backup.confirm.restore': 'backup.confirm.restore': 'Obnovit zálohu „{name}"?\n\nVšechna aktuální data budou nahrazena zálohou.',
'Obnovit zálohu „{name}"?\n\nVšechna aktuální data budou nahrazena zálohou.', 'backup.confirm.uploadRestore': 'Nahrát a obnovit zálohu „{name}"?\n\nVšechna aktuální data budou přepsána.',
'backup.confirm.uploadRestore':
'Nahrát a obnovit zálohu „{name}"?\n\nVšechna aktuální data budou přepsána.',
'backup.confirm.delete': 'Smazat zálohu „{name}"?', 'backup.confirm.delete': 'Smazat zálohu „{name}"?',
'backup.toast.loadError': 'Nepodařilo se načíst zálohy', 'backup.toast.loadError': 'Nepodařilo se načíst zálohy',
'backup.toast.created': 'Záloha byla úspěšně vytvořena', 'backup.toast.created': 'Záloha byla úspěšně vytvořena',
@@ -31,15 +29,13 @@ const backup: TranslationStrings = {
'backup.auto.title': 'Automatické zálohování', 'backup.auto.title': 'Automatické zálohování',
'backup.auto.subtitle': 'Automatické zálohování podle plánu', 'backup.auto.subtitle': 'Automatické zálohování podle plánu',
'backup.auto.enable': 'Povolit automatické zálohování', 'backup.auto.enable': 'Povolit automatické zálohování',
'backup.auto.enableHint': 'backup.auto.enableHint': 'Zálohy budou vytvářeny automaticky podle zvoleného plánu',
'Zálohy budou vytvářeny automaticky podle zvoleného plánu',
'backup.auto.interval': 'Interval', 'backup.auto.interval': 'Interval',
'backup.auto.hour': 'Spustit v hodinu', 'backup.auto.hour': 'Spustit v hodinu',
'backup.auto.hourHint': 'Místní čas serveru (formát {format})', 'backup.auto.hourHint': 'Místní čas serveru (formát {format})',
'backup.auto.dayOfWeek': 'Den v týdnu', 'backup.auto.dayOfWeek': 'Den v týdnu',
'backup.auto.dayOfMonth': 'Den v měsíci', 'backup.auto.dayOfMonth': 'Den v měsíci',
'backup.auto.dayOfMonthHint': 'backup.auto.dayOfMonthHint': 'Omezeno na 128 pro kompatibilitu se všemi měsíci',
'Omezeno na 128 pro kompatibilitu se všemi měsíci',
'backup.auto.scheduleSummary': 'Plán', 'backup.auto.scheduleSummary': 'Plán',
'backup.auto.summaryDaily': 'Každý den v {hour}:00', 'backup.auto.summaryDaily': 'Každý den v {hour}:00',
'backup.auto.summaryWeekly': 'Každý {day} v {hour}:00', 'backup.auto.summaryWeekly': 'Každý {day} v {hour}:00',
+81 -76
View File
@@ -4,8 +4,7 @@ const budget: TranslationStrings = {
'budget.title': 'Rozpočet', 'budget.title': 'Rozpočet',
'budget.exportCsv': 'Exportovat CSV', 'budget.exportCsv': 'Exportovat CSV',
'budget.emptyTitle': 'Zatím nebyl vytvořen žádný rozpočet', 'budget.emptyTitle': 'Zatím nebyl vytvořen žádný rozpočet',
'budget.emptyText': 'budget.emptyText': 'Vytvořte kategorie a položky pro plánování cestovního rozpočtu',
'Vytvořte kategorie a položky pro plánování cestovního rozpočtu',
'budget.emptyPlaceholder': 'Zadejte název kategorie...', 'budget.emptyPlaceholder': 'Zadejte název kategorie...',
'budget.createCategory': 'Vytvořit kategorii', 'budget.createCategory': 'Vytvořit kategorii',
'budget.category': 'Kategorie', 'budget.category': 'Kategorie',
@@ -27,8 +26,7 @@ const budget: TranslationStrings = {
'budget.byCategory': 'Podle kategorie', 'budget.byCategory': 'Podle kategorie',
'budget.editTooltip': 'Klikněte pro úpravu', 'budget.editTooltip': 'Klikněte pro úpravu',
'budget.linkedToReservation': 'Propojeno s rezervací — název upravte tam', 'budget.linkedToReservation': 'Propojeno s rezervací — název upravte tam',
'budget.confirm.deleteCategory': 'budget.confirm.deleteCategory': 'Opravdu chcete smazat kategorii „{name}” s {count} položkami?',
'Opravdu chcete smazat kategorii „{name}” s {count} položkami?',
'budget.deleteCategory': 'Smazat kategorii', 'budget.deleteCategory': 'Smazat kategorii',
'budget.perPerson': 'Na osobu', 'budget.perPerson': 'Na osobu',
'budget.paid': 'Zaplaceno', 'budget.paid': 'Zaplaceno',
@@ -39,78 +37,85 @@ const budget: TranslationStrings = {
'Klikněte na avatar člena u rozpočtové položky pro zelené označení to znamená, že zaplatil. Vyúčtování pak ukazuje, kdo komu a kolik dluží.', 'Klikněte na avatar člena u rozpočtové položky pro zelené označení to znamená, že zaplatil. Vyúčtování pak ukazuje, kdo komu a kolik dluží.',
'budget.netBalances': 'Čisté zůstatky', 'budget.netBalances': 'Čisté zůstatky',
'budget.categoriesLabel': 'kategorie', 'budget.categoriesLabel': 'kategorie',
"costs.you": "Vy", 'costs.you': 'Vy',
"costs.youShort": "Vy", 'costs.youShort': 'Vy',
"costs.youLower": "vy", 'costs.youLower': 'vy',
"costs.youOwe": "Dlužíte", 'costs.youOwe': 'Dlužíte',
"costs.youOweSub": "Měli byste zaplatit ostatním", 'costs.youOweSub': 'Měli byste zaplatit ostatním',
"costs.youreOwed": "Dluží vám", 'costs.youreOwed': 'Dluží vám',
"costs.youreOwedSub": "Ostatní by měli zaplatit vám", 'costs.youreOwedSub': 'Ostatní by měli zaplatit vám',
"costs.totalSpend": "Celkové výdaje na cestu", 'costs.totalSpend': 'Celkové výdaje na cestu',
"costs.totalSpendSub": "Za všechny cestovatele", 'costs.totalSpendSub': 'Za všechny cestovatele',
"costs.to": "Komu", 'costs.to': 'Komu',
"costs.from": "Od", 'costs.from': 'Od',
"costs.allSettled": "Máte vše vyrovnáno", 'costs.allSettled': 'Máte vše vyrovnáno',
"costs.nothingOwed": "Nikdo vám nic nedluží", 'costs.nothingOwed': 'Nikdo vám nic nedluží',
"costs.yourShare": "Váš podíl", 'costs.yourShare': 'Váš podíl',
"costs.youPaid": "Zaplatili jste", 'costs.youPaid': 'Zaplatili jste',
"costs.expenses": "Výdaje", 'costs.expenses': 'Výdaje',
"costs.entries": "{count} položek", 'costs.entries': '{count} položek',
"costs.searchPlaceholder": "Hledat výdaje…", 'costs.searchPlaceholder': 'Hledat výdaje…',
"costs.filter.all": "Vše", 'costs.filter.all': 'Vše',
"costs.filter.mine": "Zaplaceno mnou", 'costs.filter.mine': 'Zaplaceno mnou',
"costs.filter.owed": "Dluží mi", 'costs.filter.owed': 'Dluží mi',
"costs.addExpense": "Přidat výdaj", 'costs.addExpense': 'Přidat výdaj',
"costs.editExpense": "Upravit výdaj", 'costs.editExpense': 'Upravit výdaj',
"costs.noMatch": "Žádné výdaje neodpovídají vašemu hledání.", 'costs.noMatch': 'Žádné výdaje neodpovídají vašemu hledání.',
"costs.emptyText": "Zatím žádné výdaje. Přidejte první.", 'costs.emptyText': 'Zatím žádné výdaje. Přidejte první.',
"costs.spent": "Utraceno {amount}", 'costs.spent': 'Utraceno {amount}',
"costs.noDate": "Bez data", 'costs.noDate': 'Bez data',
"costs.noOnePaid": "Zatím nikdo nezaplatil", 'costs.noOnePaid': 'Zatím nikdo nezaplatil',
"costs.youLent": "půjčili jste {amount}", 'costs.youLent': 'půjčili jste {amount}',
"costs.youBorrowed": "vypůjčili jste si {amount}", 'costs.youBorrowed': 'vypůjčili jste si {amount}',
"costs.settleUp": "Vyrovnat", 'costs.settleUp': 'Vyrovnat',
"costs.history": "Historie", 'costs.history': 'Historie',
"costs.everyoneSquare": "Všichni jsou vyrovnáni", 'costs.everyoneSquare': 'Všichni jsou vyrovnáni',
"costs.nothingOutstanding": "Momentálně žádné nevyrovnané platby.", 'costs.nothingOutstanding': 'Momentálně žádné nevyrovnané platby.',
"costs.pay": "zaplatí", 'costs.pay': 'zaplatí',
"costs.pays": "zaplatí", 'costs.pays': 'zaplatí',
"costs.settle": "Vyrovnat", 'costs.settle': 'Vyrovnat',
"costs.balances": "Zůstatky", 'costs.balances': 'Zůstatky',
"costs.byCategory": "Podle kategorie", 'costs.byCategory': 'Podle kategorie',
"costs.noCategories": "Zatím žádné výdaje.", 'costs.noCategories': 'Zatím žádné výdaje.',
"costs.settleHistory": "Historie vyrovnání", 'costs.settleHistory': 'Historie vyrovnání',
"costs.noSettlements": "Zatím žádné vyrovnané platby.", 'costs.noSettlements': 'Zatím žádné vyrovnané platby.',
"costs.paymentsSettled": "{count} plateb vyrovnáno", 'costs.paymentsSettled': '{count} plateb vyrovnáno',
"costs.paid": "zaplaceno", 'costs.paid': 'zaplaceno',
"costs.undo": "Vrátit zpět", 'costs.undo': 'Vrátit zpět',
"costs.whatFor": "Za co to bylo?", 'costs.whatFor': 'Za co to bylo?',
"costs.namePlaceholder": "např. večeře, suvenýry, benzín…", 'costs.namePlaceholder': 'např. večeře, suvenýry, benzín…',
"costs.totalAmount": "Celková částka", 'costs.totalAmount': 'Celková částka',
"costs.currency": "Měna", 'costs.currency': 'Měna',
"costs.day": "Den", 'costs.day': 'Den',
"costs.rateLabel": "1 {from} v {to}", 'costs.rateLabel': '1 {from} v {to}',
"costs.category": "Kategorie", 'costs.category': 'Kategorie',
"costs.whoPaid": "Kdo zaplatil?", 'costs.whoPaid': 'Kdo zaplatil?',
"costs.splitBetween": "Rozdělit rovným dílem mezi", 'costs.splitBetween': 'Rozdělit rovným dílem mezi',
"costs.pickSomeone": "Vyberte alespoň jednu osobu pro rozdělení.", 'costs.pickSomeone': 'Vyberte alespoň jednu osobu pro rozdělení.',
"costs.splitSummary": "Rozděleno na {count} dílů · {amount} každý", 'costs.splitSummary': 'Rozděleno na {count} dílů · {amount} každý',
"costs.cat.accommodation": "Ubytování", 'costs.cat.accommodation': 'Ubytování',
"costs.cat.food": "Jídlo a pití", 'costs.cat.food': 'Jídlo a pití',
"costs.cat.groceries": "Potraviny", 'costs.cat.groceries': 'Potraviny',
"costs.cat.transport": "Doprava", 'costs.cat.transport': 'Doprava',
"costs.cat.flights": "Lety", 'costs.cat.flights': 'Lety',
"costs.cat.activities": "Aktivity", 'costs.cat.activities': 'Aktivity',
"costs.cat.sightseeing": "Prohlídka památek", 'costs.cat.sightseeing': 'Prohlídka památek',
"costs.cat.shopping": "Nákupy", 'costs.cat.shopping': 'Nákupy',
"costs.cat.fees": "Poplatky a vstupenky", 'costs.cat.fees': 'Poplatky a vstupenky',
"costs.cat.health": "Zdraví", 'costs.cat.health': 'Zdraví',
"costs.cat.tips": "Spropitné", 'costs.cat.tips': 'Spropitné',
"costs.cat.other": "Ostatní", 'costs.cat.other': 'Ostatní',
"costs.daysCount": "{count} dní", 'costs.daysCount': '{count} dní',
"costs.travelers": "{count} cestovatelů", 'costs.travelers': '{count} cestovatelů',
"costs.liveRate": "aktuální kurz", 'costs.liveRate': 'aktuální kurz',
"costs.settleAll": "Vyrovnat vše", 'costs.settleAll': 'Vyrovnat vše',
'costs.payment': 'Platba',
'costs.editPayment': 'Upravit platbu',
'costs.addPayment': 'Přidat platbu',
'costs.unfinished': 'Nedokončeno',
'costs.unfinishedHint': 'Jen v součtu — zatím nevyrovnáno',
'costs.tapToInclude': 'Klepnutím zahrnout',
'costs.amount': 'Částka',
}; };
export default budget; export default budget;
+1 -2
View File
@@ -13,8 +13,7 @@ const categories: TranslationStrings = {
'categories.defaultName': 'Kategorie', 'categories.defaultName': 'Kategorie',
'categories.update': 'Aktualizovat', 'categories.update': 'Aktualizovat',
'categories.create': 'Vytvořit', 'categories.create': 'Vytvořit',
'categories.confirm.delete': 'categories.confirm.delete': 'Smazat kategorii? Místa v této kategorii nebudou smazána.',
'Smazat kategorii? Místa v této kategorii nebudou smazána.',
'categories.toast.loadError': 'Nepodařilo se načíst kategorie', 'categories.toast.loadError': 'Nepodařilo se načíst kategorie',
'categories.toast.nameRequired': 'Prosím zadejte název', 'categories.toast.nameRequired': 'Prosím zadejte název',
'categories.toast.updated': 'Kategorie aktualizována', 'categories.toast.updated': 'Kategorie aktualizována',
+1 -2
View File
@@ -14,8 +14,7 @@ const collab: TranslationStrings = {
'collab.chat.placeholder': 'Napište zprávu...', 'collab.chat.placeholder': 'Napište zprávu...',
'collab.chat.empty': 'Začněte konverzaci', 'collab.chat.empty': 'Začněte konverzaci',
'collab.chat.emptyHint': 'Zprávy jsou sdíleny se všemi členy cesty', 'collab.chat.emptyHint': 'Zprávy jsou sdíleny se všemi členy cesty',
'collab.chat.emptyDesc': 'collab.chat.emptyDesc': 'Sdílejte nápady, plány a novinky se svou cestovatelskou skupinou',
'Sdílejte nápady, plány a novinky se svou cestovatelskou skupinou',
'collab.chat.today': 'Dnes', 'collab.chat.today': 'Dnes',
'collab.chat.yesterday': 'Včera', 'collab.chat.yesterday': 'Včera',
'collab.chat.deletedMessage': 'smazal zprávu', 'collab.chat.deletedMessage': 'smazal zprávu',
+4 -8
View File
@@ -20,8 +20,7 @@ const dashboard: TranslationStrings = {
'dashboard.timezoneCustomTzPlaceholder': 'např. America/New_York', 'dashboard.timezoneCustomTzPlaceholder': 'např. America/New_York',
'dashboard.timezoneCustomAdd': 'Přidat', 'dashboard.timezoneCustomAdd': 'Přidat',
'dashboard.timezoneCustomErrorEmpty': 'Zadejte identifikátor pásma', 'dashboard.timezoneCustomErrorEmpty': 'Zadejte identifikátor pásma',
'dashboard.timezoneCustomErrorInvalid': 'dashboard.timezoneCustomErrorInvalid': 'Neplatné pásmo. Použijte formát jako např. Europe/Prague',
'Neplatné pásmo. Použijte formát jako např. Europe/Prague',
'dashboard.timezoneCustomErrorDuplicate': 'Již bylo přidáno', 'dashboard.timezoneCustomErrorDuplicate': 'Již bylo přidáno',
'dashboard.emptyTitle': 'Zatím žádné cesty', 'dashboard.emptyTitle': 'Zatím žádné cesty',
'dashboard.emptyText': 'Vytvořte svou první cestu a začněte plánovat!', 'dashboard.emptyText': 'Vytvořte svou první cestu a začněte plánovat!',
@@ -55,8 +54,7 @@ const dashboard: TranslationStrings = {
'dashboard.toast.restoreError': 'Nepodařilo se obnovit cestu', 'dashboard.toast.restoreError': 'Nepodařilo se obnovit cestu',
'dashboard.toast.copied': 'Cesta byla zkopírována!', 'dashboard.toast.copied': 'Cesta byla zkopírována!',
'dashboard.toast.copyError': 'Nepodařilo se zkopírovat cestu', 'dashboard.toast.copyError': 'Nepodařilo se zkopírovat cestu',
'dashboard.confirm.delete': 'dashboard.confirm.delete': 'Smazat cestu „{title}”? Všechna místa a plány budou trvale smazány.',
'Smazat cestu „{title}”? Všechna místa a plány budou trvale smazány.',
'dashboard.editTrip': 'Upravit cestu', 'dashboard.editTrip': 'Upravit cestu',
'dashboard.createTrip': 'Vytvořit novou cestu', 'dashboard.createTrip': 'Vytvořit novou cestu',
'dashboard.tripTitle': 'Název', 'dashboard.tripTitle': 'Název',
@@ -66,10 +64,8 @@ const dashboard: TranslationStrings = {
'dashboard.startDate': 'Datum začátku', 'dashboard.startDate': 'Datum začátku',
'dashboard.endDate': 'Datum konce', 'dashboard.endDate': 'Datum konce',
'dashboard.dayCount': 'Počet dnů', 'dashboard.dayCount': 'Počet dnů',
'dashboard.dayCountHint': 'dashboard.dayCountHint': 'Kolik dnů naplánovat, když nejsou nastavena data cesty.',
'Kolik dnů naplánovat, když nejsou nastavena data cesty.', 'dashboard.noDateHint': 'Datum nezadáno výchozí délka nastavena na 7 dní. Toto lze kdykoli změnit.',
'dashboard.noDateHint':
'Datum nezadáno výchozí délka nastavena na 7 dní. Toto lze kdykoli změnit.',
'dashboard.coverImage': 'Úvodní obrázek', 'dashboard.coverImage': 'Úvodní obrázek',
'dashboard.addCoverImage': 'Vybrat úvodní obrázek (nebo přetáhnout sem)', 'dashboard.addCoverImage': 'Vybrat úvodní obrázek (nebo přetáhnout sem)',
'dashboard.addMembers': 'Spolucestující', 'dashboard.addMembers': 'Spolucestující',
+2 -4
View File
@@ -7,10 +7,8 @@ const day: TranslationStrings = {
'day.sunrise': 'Východ slunce', 'day.sunrise': 'Východ slunce',
'day.sunset': 'Západ slunce', 'day.sunset': 'Západ slunce',
'day.hourlyForecast': 'Hodinová předpověď', 'day.hourlyForecast': 'Hodinová předpověď',
'day.climateHint': 'day.climateHint': 'Historické průměry — reálná předpověď je k dispozici do 16 dnů od tohoto data.',
'Historické průměry — reálná předpověď je k dispozici do 16 dnů od tohoto data.', 'day.noWeather': 'Nejsou k dispozici žádná data o počasí. Přidejte místo se souřadnicemi.',
'day.noWeather':
'Nejsou k dispozici žádná data o počasí. Přidejte místo se souřadnicemi.',
'day.overview': 'Denní přehled', 'day.overview': 'Denní přehled',
'day.accommodation': 'Ubytování', 'day.accommodation': 'Ubytování',
'day.addAccommodation': 'Přidat ubytování', 'day.addAccommodation': 'Přidat ubytování',
+6 -12
View File
@@ -17,30 +17,24 @@ const dayplan: TranslationStrings = {
'dayplan.optimize': 'Optimalizovat', 'dayplan.optimize': 'Optimalizovat',
'dayplan.optimized': 'Trasa optimalizována', 'dayplan.optimized': 'Trasa optimalizována',
'dayplan.routeError': 'Nepodařilo se vypočítat trasu', 'dayplan.routeError': 'Nepodařilo se vypočítat trasu',
'dayplan.toast.needTwoPlaces': 'dayplan.toast.needTwoPlaces': 'Pro optimalizaci trasy jsou potřeba alespoň dvě místa',
'Pro optimalizaci trasy jsou potřeba alespoň dvě místa',
'dayplan.toast.routeOptimized': 'Trasa byla optimalizována', 'dayplan.toast.routeOptimized': 'Trasa byla optimalizována',
'dayplan.toast.routeOptimizedFromHotel': 'dayplan.toast.routeOptimizedFromHotel': 'Trasa byla optimalizována od vašeho ubytování',
'Trasa byla optimalizována od vašeho ubytování', 'dayplan.toast.noGeoPlaces': 'Nebyla nalezena žádná místa se souřadnicemi pro výpočet trasy',
'dayplan.toast.noGeoPlaces':
'Nebyla nalezena žádná místa se souřadnicemi pro výpočet trasy',
'dayplan.confirmed': 'Potvrzeno', 'dayplan.confirmed': 'Potvrzeno',
'dayplan.pendingRes': 'Čeká na potvrzení', 'dayplan.pendingRes': 'Čeká na potvrzení',
'dayplan.pdf': 'PDF', 'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exportovat denní plán do PDF', 'dayplan.pdfTooltip': 'Exportovat denní plán do PDF',
'dayplan.pdfError': 'Export do PDF se nezdařil', 'dayplan.pdfError': 'Export do PDF se nezdařil',
'dayplan.cannotReorderTransport': 'dayplan.cannotReorderTransport': 'Rezervace s pevným časem nelze přeuspořádat',
'Rezervace s pevným časem nelze přeuspořádat',
'dayplan.confirmRemoveTimeTitle': 'Odebrat čas?', 'dayplan.confirmRemoveTimeTitle': 'Odebrat čas?',
'dayplan.confirmRemoveTimeBody': 'dayplan.confirmRemoveTimeBody':
'Toto místo má pevný čas ({time}). Přesunutím se čas odebere a povolí se volné řazení.', 'Toto místo má pevný čas ({time}). Přesunutím se čas odebere a povolí se volné řazení.',
'dayplan.confirmRemoveTimeAction': 'Odebrat čas a přesunout', 'dayplan.confirmRemoveTimeAction': 'Odebrat čas a přesunout',
'dayplan.confirmDeleteNoteTitle': 'Smazat poznámku?', 'dayplan.confirmDeleteNoteTitle': 'Smazat poznámku?',
'dayplan.confirmDeleteNoteBody': 'Tato poznámka bude trvale smazána.', 'dayplan.confirmDeleteNoteBody': 'Tato poznámka bude trvale smazána.',
'dayplan.cannotDropOnTimed': 'dayplan.cannotDropOnTimed': 'Položky nelze umístit mezi záznamy s pevným časem',
'Položky nelze umístit mezi záznamy s pevným časem', 'dayplan.cannotBreakChronology': 'Toto by porušilo chronologické pořadí naplánovaných položek a rezervací',
'dayplan.cannotBreakChronology':
'Toto by porušilo chronologické pořadí naplánovaných položek a rezervací',
'dayplan.mobile.addPlace': 'Přidat místo', 'dayplan.mobile.addPlace': 'Přidat místo',
'dayplan.mobile.searchPlaces': 'Hledat místa...', 'dayplan.mobile.searchPlaces': 'Hledat místa...',
'dayplan.mobile.allAssigned': 'Všechna místa přiřazena', 'dayplan.mobile.allAssigned': 'Všechna místa přiřazena',
+1 -2
View File
@@ -55,8 +55,7 @@ const cs: NotificationLocale = {
body: 'Obdrželi jsme žádost o obnovení hesla k tvému účtu TREK. Klikni na tlačítko níže a nastav nové heslo.', body: 'Obdrželi jsme žádost o obnovení hesla k tvému účtu TREK. Klikni na tlačítko níže a nastav nové heslo.',
ctaIntro: 'Obnovit heslo', ctaIntro: 'Obnovit heslo',
expiry: 'Odkaz vyprší za 60 minut.', expiry: 'Odkaz vyprší za 60 minut.',
ignore: ignore: 'Pokud jsi o obnovení nežádal/a, tento e-mail ignoruj — heslo zůstane beze změny.',
'Pokud jsi o obnovení nežádal/a, tento e-mail ignoruj — heslo zůstane beze změny.',
}, },
}; };
+3 -6
View File
@@ -13,8 +13,7 @@ const files: TranslationStrings = {
'files.uploadError': 'Nahrávání se nezdařilo', 'files.uploadError': 'Nahrávání se nezdařilo',
'files.dropzone': 'Přetáhněte soubory sem', 'files.dropzone': 'Přetáhněte soubory sem',
'files.dropzoneHint': 'nebo klikněte pro výběr', 'files.dropzoneHint': 'nebo klikněte pro výběr',
'files.allowedTypes': 'files.allowedTypes': 'Obrázky, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Max 50 MB',
'Obrázky, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Max 50 MB',
'files.uploading': 'Nahrávání...', 'files.uploading': 'Nahrávání...',
'files.filterAll': 'Vše', 'files.filterAll': 'Vše',
'files.filterPdf': 'PDF', 'files.filterPdf': 'PDF',
@@ -52,10 +51,8 @@ const files: TranslationStrings = {
'files.toast.assigned': 'Soubor byl přiřazen', 'files.toast.assigned': 'Soubor byl přiřazen',
'files.toast.assignError': 'Přiřazení se nezdařilo', 'files.toast.assignError': 'Přiřazení se nezdařilo',
'files.toast.restoreError': 'Obnovení se nezdařilo', 'files.toast.restoreError': 'Obnovení se nezdařilo',
'files.confirm.permanentDelete': 'files.confirm.permanentDelete': 'Trvale smazat tento soubor? Tuto akci nelze vrátit.',
'Trvale smazat tento soubor? Tuto akci nelze vrátit.', 'files.confirm.emptyTrash': 'Trvale smazat všechny soubory v koši? Tuto akci nelze vrátit.',
'files.confirm.emptyTrash':
'Trvale smazat všechny soubory v koši? Tuto akci nelze vrátit.',
'files.noteLabel': 'Poznámka', 'files.noteLabel': 'Poznámka',
'files.notePlaceholder': 'Přidat poznámku...', 'files.notePlaceholder': 'Přidat poznámku...',
}; };
+13 -26
View File
@@ -14,14 +14,12 @@ const journey: TranslationStrings = {
'journey.createError': 'Nepodařilo se vytvořit cestovní deník', 'journey.createError': 'Nepodařilo se vytvořit cestovní deník',
'journey.deleteError': 'Nepodařilo se smazat cestovní deník', 'journey.deleteError': 'Nepodařilo se smazat cestovní deník',
'journey.deleteConfirmTitle': 'Smazat', 'journey.deleteConfirmTitle': 'Smazat',
'journey.deleteConfirmMessage': 'journey.deleteConfirmMessage': 'Smazat „{title}"? Tuto akci nelze vrátit zpět.',
'Smazat „{title}"? Tuto akci nelze vrátit zpět.',
'journey.deleteConfirmGeneric': 'Opravdu to chcete smazat?', 'journey.deleteConfirmGeneric': 'Opravdu to chcete smazat?',
'journey.notFound': 'Cestovní deník nenalezen', 'journey.notFound': 'Cestovní deník nenalezen',
'journey.photos': 'Fotky', 'journey.photos': 'Fotky',
'journey.timelineEmpty': 'Zatím žádné zastávky', 'journey.timelineEmpty': 'Zatím žádné zastávky',
'journey.timelineEmptyHint': 'journey.timelineEmptyHint': 'Přidejte odbavení nebo napište záznam do deníku',
'Přidejte odbavení nebo napište záznam do deníku',
'journey.status.draft': 'Koncept', 'journey.status.draft': 'Koncept',
'journey.status.active': 'Aktivní', 'journey.status.active': 'Aktivní',
'journey.status.completed': 'Dokončeno', 'journey.status.completed': 'Dokončeno',
@@ -47,30 +45,25 @@ const journey: TranslationStrings = {
'journey.editor.titlePlaceholder': 'Pojmenujte tento okamžik...', 'journey.editor.titlePlaceholder': 'Pojmenujte tento okamžik...',
'journey.editor.bodyPlaceholder': 'Vyprávějte příběh tohoto dne...', 'journey.editor.bodyPlaceholder': 'Vyprávějte příběh tohoto dne...',
'journey.editor.placePlaceholder': 'Místo (volitelné)', 'journey.editor.placePlaceholder': 'Místo (volitelné)',
'journey.editor.tagsPlaceholder': 'journey.editor.tagsPlaceholder': 'Tagy: skrytý klenot, nejlepší jídlo, musím se vrátit...',
'Tagy: skrytý klenot, nejlepší jídlo, musím se vrátit...',
'journey.visibility.private': 'Soukromý', 'journey.visibility.private': 'Soukromý',
'journey.visibility.shared': 'Sdílený', 'journey.visibility.shared': 'Sdílený',
'journey.visibility.public': 'Veřejný', 'journey.visibility.public': 'Veřejný',
'journey.emptyState.title': 'Váš příběh začíná zde', 'journey.emptyState.title': 'Váš příběh začíná zde',
'journey.emptyState.subtitle': 'journey.emptyState.subtitle': 'Odbavte se na místě nebo napište svůj první záznam do deníku',
'Odbavte se na místě nebo napište svůj první záznam do deníku', 'journey.frontpage.subtitle': 'Proměňte své cesty v příběhy, na které nikdy nezapomenete',
'journey.frontpage.subtitle':
'Proměňte své cesty v příběhy, na které nikdy nezapomenete',
'journey.frontpage.createJourney': 'Vytvořit cestovní deník', 'journey.frontpage.createJourney': 'Vytvořit cestovní deník',
'journey.frontpage.activeJourney': 'Aktivní cestovní deník', 'journey.frontpage.activeJourney': 'Aktivní cestovní deník',
'journey.frontpage.allJourneys': 'Všechny cestovní deníky', 'journey.frontpage.allJourneys': 'Všechny cestovní deníky',
'journey.frontpage.journeys': 'cestovní deníky', 'journey.frontpage.journeys': 'cestovní deníky',
'journey.frontpage.createNew': 'Vytvořit nový cestovní deník', 'journey.frontpage.createNew': 'Vytvořit nový cestovní deník',
'journey.frontpage.createNewSub': 'journey.frontpage.createNewSub': 'Vyberte cesty, pište příběhy, sdílejte dobrodružství',
'Vyberte cesty, pište příběhy, sdílejte dobrodružství',
'journey.frontpage.live': 'Živě', 'journey.frontpage.live': 'Živě',
'journey.frontpage.synced': 'Synchronizováno', 'journey.frontpage.synced': 'Synchronizováno',
'journey.frontpage.continueWriting': 'Pokračovat v psaní', 'journey.frontpage.continueWriting': 'Pokračovat v psaní',
'journey.frontpage.updated': 'Aktualizováno {time}', 'journey.frontpage.updated': 'Aktualizováno {time}',
'journey.frontpage.suggestionLabel': 'Cesta právě skončila', 'journey.frontpage.suggestionLabel': 'Cesta právě skončila',
'journey.frontpage.suggestionText': 'journey.frontpage.suggestionText': 'Proměňte <strong>{title}</strong> v cestovní deník',
'Proměňte <strong>{title}</strong> v cestovní deník',
'journey.frontpage.dismiss': 'Zavřít', 'journey.frontpage.dismiss': 'Zavřít',
'journey.frontpage.journeyName': 'Název cestovního deníku', 'journey.frontpage.journeyName': 'Název cestovního deníku',
'journey.frontpage.namePlaceholder': 'např. Jihovýchodní Asie 2026', 'journey.frontpage.namePlaceholder': 'např. Jihovýchodní Asie 2026',
@@ -85,11 +78,9 @@ const journey: TranslationStrings = {
'journey.detail.newEntry': 'Nový záznam', 'journey.detail.newEntry': 'Nový záznam',
'journey.detail.editEntry': 'Upravit záznam', 'journey.detail.editEntry': 'Upravit záznam',
'journey.detail.noEntries': 'Zatím žádné záznamy', 'journey.detail.noEntries': 'Zatím žádné záznamy',
'journey.detail.noEntriesHint': 'journey.detail.noEntriesHint': 'Přidejte cestu pro začátek s kostrovými záznamy',
'Přidejte cestu pro začátek s kostrovými záznamy',
'journey.detail.noPhotos': 'Zatím žádné fotky', 'journey.detail.noPhotos': 'Zatím žádné fotky',
'journey.detail.noPhotosHint': 'journey.detail.noPhotosHint': 'Nahrajte fotky k záznamům nebo procházejte knihovnu Immich/Synology',
'Nahrajte fotky k záznamům nebo procházejte knihovnu Immich/Synology',
'journey.detail.journeyStats': 'Statistiky cesty', 'journey.detail.journeyStats': 'Statistiky cesty',
'journey.detail.syncedTrips': 'Synchronizované cesty', 'journey.detail.syncedTrips': 'Synchronizované cesty',
'journey.detail.noTripsLinked': 'Zatím žádné propojené cesty', 'journey.detail.noTripsLinked': 'Zatím žádné propojené cesty',
@@ -115,8 +106,7 @@ const journey: TranslationStrings = {
'journey.editor.uploadPhotos': 'Nahrát fotky', 'journey.editor.uploadPhotos': 'Nahrát fotky',
'journey.editor.uploading': 'Nahrávání...', 'journey.editor.uploading': 'Nahrávání...',
'journey.editor.uploadingProgress': 'Nahrávání {done}/{total}…', 'journey.editor.uploadingProgress': 'Nahrávání {done}/{total}…',
'journey.editor.uploadPartialFailed': 'journey.editor.uploadPartialFailed': '{failed} z {total} fotek selhalo — uložte znovu pro opakování',
'{failed} z {total} fotek selhalo — uložte znovu pro opakování',
'journey.editor.fromGallery': 'Z galerie', 'journey.editor.fromGallery': 'Z galerie',
'journey.editor.allPhotosAdded': 'Všechny fotky již přidány', 'journey.editor.allPhotosAdded': 'Všechny fotky již přidány',
'journey.editor.writeStory': 'Napište svůj příběh...', 'journey.editor.writeStory': 'Napište svůj příběh...',
@@ -195,12 +185,10 @@ const journey: TranslationStrings = {
'journey.settings.reopenJourney': 'Obnovit cestu', 'journey.settings.reopenJourney': 'Obnovit cestu',
'journey.settings.archived': 'Cesta archivována', 'journey.settings.archived': 'Cesta archivována',
'journey.settings.reopened': 'Cesta znovu otevřena', 'journey.settings.reopened': 'Cesta znovu otevřena',
'journey.settings.endDescription': 'journey.settings.endDescription': 'Skryje odznak Živě. Kdykoli jej lze znovu otevřít.',
'Skryje odznak Živě. Kdykoli jej lze znovu otevřít.',
'journey.settings.delete': 'Smazat', 'journey.settings.delete': 'Smazat',
'journey.settings.deleteJourney': 'Smazat cestovní deník', 'journey.settings.deleteJourney': 'Smazat cestovní deník',
'journey.settings.deleteMessage': 'journey.settings.deleteMessage': 'Smazat „{title}"? Všechny záznamy a fotky budou ztraceny.',
'Smazat „{title}"? Všechny záznamy a fotky budou ztraceny.',
'journey.settings.saved': 'Nastavení uloženo', 'journey.settings.saved': 'Nastavení uloženo',
'journey.settings.saveFailed': 'Uložení selhalo', 'journey.settings.saveFailed': 'Uložení selhalo',
'journey.settings.coverUpdated': 'Obal aktualizován', 'journey.settings.coverUpdated': 'Obal aktualizován',
@@ -211,8 +199,7 @@ const journey: TranslationStrings = {
'journey.photosUploadFailed': 'Některé fotky se nepodařilo nahrát', 'journey.photosUploadFailed': 'Některé fotky se nepodařilo nahrát',
'journey.photosAdded': '{count} fotografií přidáno', 'journey.photosAdded': '{count} fotografií přidáno',
'journey.public.notFound': 'Nenalezeno', 'journey.public.notFound': 'Nenalezeno',
'journey.public.notFoundMessage': 'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.',
'Tento cestovní deník neexistuje nebo odkaz vypršel.',
'journey.public.readOnly': 'Pouze ke čtení · Veřejný cestovní deník', 'journey.public.readOnly': 'Pouze ke čtení · Veřejný cestovní deník',
'journey.public.tagline': 'Travel Resource & Exploration Kit', 'journey.public.tagline': 'Travel Resource & Exploration Kit',
'journey.public.sharedVia': 'Sdíleno přes', 'journey.public.sharedVia': 'Sdíleno přes',
+11 -22
View File
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
const login: TranslationStrings = { const login: TranslationStrings = {
'login.error': 'Přihlášení se nezdařilo. Zkontrolujte prosím své údaje.', 'login.error': 'Přihlášení se nezdařilo. Zkontrolujte prosím své údaje.',
'login.tagline': 'Vaše cesty.\nVáš plán.', 'login.tagline': 'Vaše cesty.\nVáš plán.',
'login.description': 'login.description': 'Plánujte cesty společně s interaktivními mapami, rozpočty a synchronizací v reálném čase.',
'Plánujte cesty společně s interaktivními mapami, rozpočty a synchronizací v reálném čase.',
'login.features.maps': 'Interaktivní mapy', 'login.features.maps': 'Interaktivní mapy',
'login.features.mapsDesc': 'Google Places, trasy a shlukování bodů', 'login.features.mapsDesc': 'Google Places, trasy a shlukování bodů',
'login.features.realtime': 'Synchronizace v reálném čase', 'login.features.realtime': 'Synchronizace v reálném čase',
@@ -20,8 +19,7 @@ const login: TranslationStrings = {
'login.features.files': 'Dokumenty', 'login.features.files': 'Dokumenty',
'login.features.filesDesc': 'Nahrávejte a spravujte dokumenty', 'login.features.filesDesc': 'Nahrávejte a spravujte dokumenty',
'login.features.routes': 'Chytré trasy', 'login.features.routes': 'Chytré trasy',
'login.features.routesDesc': 'login.features.routesDesc': 'Automatická optimalizace a export do Google Maps',
'Automatická optimalizace a export do Google Maps',
'login.selfHosted': 'Self-hosted · Open Source · Vaše data zůstávají u vás', 'login.selfHosted': 'Self-hosted · Open Source · Vaše data zůstávají u vás',
'login.title': 'Přihlásit se', 'login.title': 'Přihlásit se',
'login.subtitle': 'Vítejte zpět', 'login.subtitle': 'Vítejte zpět',
@@ -39,24 +37,20 @@ const login: TranslationStrings = {
'login.register': 'Registrovat se', 'login.register': 'Registrovat se',
'login.emailPlaceholder': 'vas@email.cz', 'login.emailPlaceholder': 'vas@email.cz',
'login.username': 'Uživatelské jméno', 'login.username': 'Uživatelské jméno',
'login.oidc.registrationDisabled': 'login.oidc.registrationDisabled': 'Registrace je zakázána. Kontaktujte svého administrátora.',
'Registrace je zakázána. Kontaktujte svého administrátora.',
'login.oidc.noEmail': 'Od poskytovatele nebyl přijat žádný e-mail.', 'login.oidc.noEmail': 'Od poskytovatele nebyl přijat žádný e-mail.',
'login.oidc.tokenFailed': 'Ověření se nezdařilo.', 'login.oidc.tokenFailed': 'Ověření se nezdařilo.',
'login.oidc.invalidState': 'Neplatná relace. Zkuste to prosím znovu.', 'login.oidc.invalidState': 'Neplatná relace. Zkuste to prosím znovu.',
'login.demoFailed': 'Přihlášení do dema se nezdařilo', 'login.demoFailed': 'Přihlášení do dema se nezdařilo',
'login.oidcSignIn': 'Přihlásit se přes {name}', 'login.oidcSignIn': 'Přihlásit se přes {name}',
'login.oidcOnly': 'login.oidcOnly': 'Ověřování heslem je zakázáno. Přihlaste se prosím přes SSO poskytovatele.',
'Ověřování heslem je zakázáno. Přihlaste se prosím přes SSO poskytovatele.', 'login.oidcLoggedOut': 'Byl jste odhlášen. Přihlaste se znovu přes SSO poskytovatele.',
'login.oidcLoggedOut':
'Byl jste odhlášen. Přihlaste se znovu přes SSO poskytovatele.',
'login.demoHint': 'Vyzkoušejte demo registrace není nutná', 'login.demoHint': 'Vyzkoušejte demo registrace není nutná',
'login.mfaTitle': 'Dvoufaktorové ověření', 'login.mfaTitle': 'Dvoufaktorové ověření',
'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.', 'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.',
'login.mfaCodeLabel': 'Ověřovací kód', 'login.mfaCodeLabel': 'Ověřovací kód',
'login.mfaCodeRequired': 'Zadejte kód z aplikace.', 'login.mfaCodeRequired': 'Zadejte kód z aplikace.',
'login.mfaHint': 'login.mfaHint': 'Otevřete Google Authenticator, Authy nebo jinou TOTP aplikaci.',
'Otevřete Google Authenticator, Authy nebo jinou TOTP aplikaci.',
'login.mfaBack': '← Zpět k přihlášení', 'login.mfaBack': '← Zpět k přihlášení',
'login.mfaVerify': 'Ověřit', 'login.mfaVerify': 'Ověřit',
'login.invalidInviteLink': 'Neplatný nebo vypršelý odkaz s pozvánkou', 'login.invalidInviteLink': 'Neplatný nebo vypršelý odkaz s pozvánkou',
@@ -66,8 +60,7 @@ const login: TranslationStrings = {
'login.forgotPassword': 'Zapomenuté heslo?', 'login.forgotPassword': 'Zapomenuté heslo?',
'login.rememberMe': 'Zapamatovat si mě', 'login.rememberMe': 'Zapamatovat si mě',
'login.forgotPasswordTitle': 'Obnovení hesla', 'login.forgotPasswordTitle': 'Obnovení hesla',
'login.forgotPasswordBody': 'login.forgotPasswordBody': 'Zadej e-mail použitý při registraci. Pokud účet existuje, pošleme odkaz pro obnovení.',
'Zadej e-mail použitý při registraci. Pokud účet existuje, pošleme odkaz pro obnovení.',
'login.forgotPasswordSubmit': 'Odeslat odkaz', 'login.forgotPasswordSubmit': 'Odeslat odkaz',
'login.forgotPasswordSentTitle': 'Zkontroluj e-mail', 'login.forgotPasswordSentTitle': 'Zkontroluj e-mail',
'login.forgotPasswordSentBody': 'login.forgotPasswordSentBody':
@@ -80,20 +73,16 @@ const login: TranslationStrings = {
'login.passwordsDontMatch': 'Hesla se neshodují', 'login.passwordsDontMatch': 'Hesla se neshodují',
'login.mfaCode': 'Kód 2FA', 'login.mfaCode': 'Kód 2FA',
'login.resetPasswordTitle': 'Nastavit nové heslo', 'login.resetPasswordTitle': 'Nastavit nové heslo',
'login.resetPasswordBody': 'login.resetPasswordBody': 'Vyber silné heslo, které jsi tu ještě nepoužil. Minimálně 8 znaků.',
'Vyber silné heslo, které jsi tu ještě nepoužil. Minimálně 8 znaků.', 'login.resetPasswordMfaBody': 'Zadej 2FA kód nebo záložní kód pro dokončení obnovení.',
'login.resetPasswordMfaBody':
'Zadej 2FA kód nebo záložní kód pro dokončení obnovení.',
'login.resetPasswordSubmit': 'Obnovit heslo', 'login.resetPasswordSubmit': 'Obnovit heslo',
'login.resetPasswordVerify': 'Ověřit a obnovit', 'login.resetPasswordVerify': 'Ověřit a obnovit',
'login.resetPasswordSuccessTitle': 'Heslo aktualizováno', 'login.resetPasswordSuccessTitle': 'Heslo aktualizováno',
'login.resetPasswordSuccessBody': 'Nyní se můžeš přihlásit novým heslem.', 'login.resetPasswordSuccessBody': 'Nyní se můžeš přihlásit novým heslem.',
'login.resetPasswordInvalidLink': 'Neplatný odkaz', 'login.resetPasswordInvalidLink': 'Neplatný odkaz',
'login.resetPasswordInvalidLinkBody': 'login.resetPasswordInvalidLinkBody': 'Odkaz chybí nebo je poškozený. Pro pokračování si vyžádej nový.',
'Odkaz chybí nebo je poškozený. Pro pokračování si vyžádej nový.',
'login.resetPasswordFailed': 'Obnovení se nezdařilo. Odkaz mohl vypršet.', 'login.resetPasswordFailed': 'Obnovení se nezdařilo. Odkaz mohl vypršet.',
'login.passkey.signIn': 'Přihlásit se pomocí přístupového klíče', 'login.passkey.signIn': 'Přihlásit se pomocí přístupového klíče',
'login.passkey.failed': 'login.passkey.failed': 'Přihlášení přístupovým klíčem se nezdařilo. Zkuste to prosím znovu.',
'Přihlášení přístupovým klíčem se nezdařilo. Zkuste to prosím znovu.',
}; };
export default login; export default login;
+6 -12
View File
@@ -3,14 +3,12 @@ import type { TranslationStrings } from '../types';
const memories: TranslationStrings = { const memories: TranslationStrings = {
'memories.title': 'Fotky', 'memories.title': 'Fotky',
'memories.notConnected': 'Immich není připojen', 'memories.notConnected': 'Immich není připojen',
'memories.notConnectedHint': 'memories.notConnectedHint': 'Připojte svoji instanci Immich v Nastavení, abyste zde viděli fotky z cest.',
'Připojte svoji instanci Immich v Nastavení, abyste zde viděli fotky z cest.',
'memories.notConnectedMultipleHint': 'memories.notConnectedMultipleHint':
'Pro přidání fotek k tomuto výletu připojte v Nastavení jednoho z těchto poskytovatelů fotek: {provider_names}.', 'Pro přidání fotek k tomuto výletu připojte v Nastavení jednoho z těchto poskytovatelů fotek: {provider_names}.',
'memories.noDates': 'Přidejte data k cestě pro načtení fotek.', 'memories.noDates': 'Přidejte data k cestě pro načtení fotek.',
'memories.noPhotos': 'Nenalezeny žádné fotky', 'memories.noPhotos': 'Nenalezeny žádné fotky',
'memories.noPhotosHint': 'memories.noPhotosHint': 'V Immich nebyly nalezeny žádné fotky pro období této cesty.',
'V Immich nebyly nalezeny žádné fotky pro období této cesty.',
'memories.photosFound': 'fotek', 'memories.photosFound': 'fotek',
'memories.fromOthers': 'od ostatních', 'memories.fromOthers': 'od ostatních',
'memories.sharePhotos': 'Sdílet fotky', 'memories.sharePhotos': 'Sdílet fotky',
@@ -24,10 +22,8 @@ const memories: TranslationStrings = {
'memories.providerPassword': 'Heslo', 'memories.providerPassword': 'Heslo',
'memories.providerOTP': 'MFA kód (pokud je povoleno)', 'memories.providerOTP': 'MFA kód (pokud je povoleno)',
'memories.skipSSLVerification': 'Přeskočit ověření SSL certifikátu', 'memories.skipSSLVerification': 'Přeskočit ověření SSL certifikátu',
'memories.immichAutoUpload': 'memories.immichAutoUpload': 'Zrcadlit fotky journey při nahrávání také do Immich',
'Zrcadlit fotky journey při nahrávání také do Immich', 'memories.providerUrlHintSynology': 'Zahrňte cestu aplikace Photos do URL, např. https://nas:5001/photo',
'memories.providerUrlHintSynology':
'Zahrňte cestu aplikace Photos do URL, např. https://nas:5001/photo',
'memories.testConnection': 'Otestovat připojení', 'memories.testConnection': 'Otestovat připojení',
'memories.testShort': 'Otestovat', 'memories.testShort': 'Otestovat',
'memories.testFirst': 'Nejprve otestujte připojení', 'memories.testFirst': 'Nejprve otestujte připojení',
@@ -72,10 +68,8 @@ const memories: TranslationStrings = {
'memories.error.addPhotos': 'Přidání fotek se nezdařilo', 'memories.error.addPhotos': 'Přidání fotek se nezdařilo',
'memories.error.removePhoto': 'Odebrání fotky se nezdařilo', 'memories.error.removePhoto': 'Odebrání fotky se nezdařilo',
'memories.error.toggleSharing': 'Aktualizace sdílení se nezdařila', 'memories.error.toggleSharing': 'Aktualizace sdílení se nezdařila',
'memories.saveRouteNotConfigured': 'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele',
'Trasa uložení není nakonfigurována pro tohoto poskytovatele', 'memories.testRouteNotConfigured': 'Testovací trasa 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.fillRequiredFields': 'Prosím vyplňte všechna povinná pole',
}; };
export default memories; export default memories;
+2 -4
View File
@@ -14,8 +14,7 @@ const notif: TranslationStrings = {
'notif.todo_due.title': 'Úkol se blíží', 'notif.todo_due.title': 'Úkol se blíží',
'notif.todo_due.text': '{todo} ve výletě {trip} má termín {due}', 'notif.todo_due.text': '{todo} ve výletě {trip} má termín {due}',
'notif.vacay_invite.title': 'Pozvánka Vacay Fusion', 'notif.vacay_invite.title': 'Pozvánka Vacay Fusion',
'notif.vacay_invite.text': 'notif.vacay_invite.text': '{actor} vás pozval ke spojení dovolenkových plánů',
'{actor} vás pozval ke spojení dovolenkových plánů',
'notif.photos_shared.title': 'Fotky sdíleny', 'notif.photos_shared.title': 'Fotky sdíleny',
'notif.photos_shared.text': '{actor} sdílel {count} foto v {trip}', 'notif.photos_shared.text': '{actor} sdílel {count} foto v {trip}',
'notif.collab_message.title': 'Nová zpráva', 'notif.collab_message.title': 'Nová zpráva',
@@ -36,7 +35,6 @@ const notif: TranslationStrings = {
'notif.generic.title': 'Oznámení', 'notif.generic.title': 'Oznámení',
'notif.generic.text': 'Máte nové oznámení', 'notif.generic.text': 'Máte nové oznámení',
'notif.dev.unknown_event.title': '[DEV] Neznámá událost', 'notif.dev.unknown_event.title': '[DEV] Neznámá událost',
'notif.dev.unknown_event.text': 'notif.dev.unknown_event.text': 'Typ události "{event}" není registrován v EVENT_NOTIFICATION_CONFIG',
'Typ události "{event}" není registrován v EVENT_NOTIFICATION_CONFIG',
}; };
export default notif; export default notif;
+1 -2
View File
@@ -26,8 +26,7 @@ const notifications: TranslationStrings = {
'notifications.test.navigateText': 'Testovací navigační oznámení.', 'notifications.test.navigateText': 'Testovací navigační oznámení.',
'notifications.test.goThere': 'Přejít tam', 'notifications.test.goThere': 'Přejít tam',
'notifications.test.adminTitle': 'Hromadná zpráva pro správce', 'notifications.test.adminTitle': 'Hromadná zpráva pro správce',
'notifications.test.adminText': 'notifications.test.adminText': '{actor} odeslal testovací oznámení všem správcům.',
'{actor} odeslal testovací oznámení všem správcům.',
'notifications.test.tripTitle': '{actor} přispěl do vašeho výletu', 'notifications.test.tripTitle': '{actor} přispěl do vašeho výletu',
'notifications.test.tripText': 'Testovací oznámení pro výlet "{trip}".', 'notifications.test.tripText': 'Testovací oznámení pro výlet "{trip}".',
'notifications.versionAvailable.title': 'Dostupná aktualizace', 'notifications.versionAvailable.title': 'Dostupná aktualizace',
+31 -62
View File
@@ -17,95 +17,66 @@ const oauth: TranslationStrings = {
'oauth.scope.trips:read.label': 'Zobrazit výlety a itineráře', 'oauth.scope.trips:read.label': 'Zobrazit výlety a itineráře',
'oauth.scope.trips:read.description': 'Číst výlety, dny, poznámky a členy', 'oauth.scope.trips:read.description': 'Číst výlety, dny, poznámky a členy',
'oauth.scope.trips:write.label': 'Upravit výlety a itineráře', 'oauth.scope.trips:write.label': 'Upravit výlety a itineráře',
'oauth.scope.trips:write.description': 'oauth.scope.trips:write.description': 'Vytvářet a aktualizovat výlety, dny, poznámky a spravovat členy',
'Vytvářet a aktualizovat výlety, dny, poznámky a spravovat členy',
'oauth.scope.trips:delete.label': 'Mazat výlety', 'oauth.scope.trips:delete.label': 'Mazat výlety',
'oauth.scope.trips:delete.description': 'oauth.scope.trips:delete.description': 'Trvale smazat celé výlety — tato akce je nevratná',
'Trvale smazat celé výlety — tato akce je nevratná',
'oauth.scope.trips:share.label': 'Spravovat sdílené odkazy', 'oauth.scope.trips:share.label': 'Spravovat sdílené odkazy',
'oauth.scope.trips:share.description': 'oauth.scope.trips:share.description': 'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy',
'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy',
'oauth.scope.places:read.label': 'Zobrazit místa a mapová data', 'oauth.scope.places:read.label': 'Zobrazit místa a mapová data',
'oauth.scope.places:read.description': 'oauth.scope.places:read.description': 'Číst místa, denní přiřazení, štítky a kategorie',
'Číst místa, denní přiřazení, štítky a kategorie',
'oauth.scope.places:write.label': 'Spravovat místa', 'oauth.scope.places:write.label': 'Spravovat místa',
'oauth.scope.places:write.description': 'oauth.scope.places:write.description': 'Vytvářet, aktualizovat a mazat místa, přiřazení a štítky',
'Vytvářet, aktualizovat a mazat místa, přiřazení a štítky',
'oauth.scope.atlas:read.label': 'Zobrazit Atlas', 'oauth.scope.atlas:read.label': 'Zobrazit Atlas',
'oauth.scope.atlas:read.description': 'oauth.scope.atlas:read.description': 'Číst navštívené země, regiony a seznam přání',
'Číst navštívené země, regiony a seznam přání',
'oauth.scope.atlas:write.label': 'Spravovat Atlas', 'oauth.scope.atlas:write.label': 'Spravovat Atlas',
'oauth.scope.atlas:write.description': 'oauth.scope.atlas:write.description': 'Označovat navštívené země a regiony, spravovat seznam přání',
'Označovat navštívené země a regiony, spravovat seznam přání',
'oauth.scope.packing:read.label': 'Zobrazit seznamy balení', 'oauth.scope.packing:read.label': 'Zobrazit seznamy balení',
'oauth.scope.packing:read.description': 'oauth.scope.packing:read.description': 'Číst položky, tašky a přiřazení kategorií',
'Číst položky, tašky a přiřazení kategorií',
'oauth.scope.packing:write.label': 'Spravovat seznamy balení', 'oauth.scope.packing:write.label': 'Spravovat seznamy balení',
'oauth.scope.packing:write.description': 'oauth.scope.packing:write.description': 'Přidávat, aktualizovat, mazat, označovat a řadit položky a tašky',
'Přidávat, aktualizovat, mazat, označovat a řadit položky a tašky',
'oauth.scope.todos:read.label': 'Zobrazit seznamy úkolů', 'oauth.scope.todos:read.label': 'Zobrazit seznamy úkolů',
'oauth.scope.todos:read.description': 'oauth.scope.todos:read.description': 'Číst úkoly výletu a přiřazení kategorií',
'Číst úkoly výletu a přiřazení kategorií',
'oauth.scope.todos:write.label': 'Spravovat seznamy úkolů', 'oauth.scope.todos:write.label': 'Spravovat seznamy úkolů',
'oauth.scope.todos:write.description': 'oauth.scope.todos:write.description': 'Vytvářet, aktualizovat, označovat, mazat a řadit úkoly',
'Vytvářet, aktualizovat, označovat, mazat a řadit úkoly',
'oauth.scope.budget:read.label': 'Zobrazit rozpočet', 'oauth.scope.budget:read.label': 'Zobrazit rozpočet',
'oauth.scope.budget:read.description': 'oauth.scope.budget:read.description': 'Číst položky rozpočtu a přehled výdajů',
'Číst položky rozpočtu a přehled výdajů',
'oauth.scope.budget:write.label': 'Spravovat rozpočet', 'oauth.scope.budget:write.label': 'Spravovat rozpočet',
'oauth.scope.budget:write.description': 'oauth.scope.budget:write.description': 'Vytvářet, aktualizovat a mazat položky rozpočtu',
'Vytvářet, aktualizovat a mazat položky rozpočtu',
'oauth.scope.reservations:read.label': 'Zobrazit rezervace', 'oauth.scope.reservations:read.label': 'Zobrazit rezervace',
'oauth.scope.reservations:read.description': 'oauth.scope.reservations:read.description': 'Číst rezervace a podrobnosti ubytování',
'Číst rezervace a podrobnosti ubytování',
'oauth.scope.reservations:write.label': 'Spravovat rezervace', 'oauth.scope.reservations:write.label': 'Spravovat rezervace',
'oauth.scope.reservations:write.description': 'oauth.scope.reservations:write.description': 'Vytvářet, aktualizovat, mazat a řadit rezervace',
'Vytvářet, aktualizovat, mazat a řadit rezervace',
'oauth.scope.collab:read.label': 'Zobrazit spolupráci', 'oauth.scope.collab:read.label': 'Zobrazit spolupráci',
'oauth.scope.collab:read.description': 'oauth.scope.collab:read.description': 'Číst poznámky, ankety a zprávy spolupráce',
'Číst poznámky, ankety a zprávy spolupráce',
'oauth.scope.collab:write.label': 'Spravovat spolupráci', 'oauth.scope.collab:write.label': 'Spravovat spolupráci',
'oauth.scope.collab:write.description': 'oauth.scope.collab:write.description': 'Vytvářet, aktualizovat a mazat poznámky, ankety a zprávy',
'Vytvářet, aktualizovat a mazat poznámky, ankety a zprávy',
'oauth.scope.notifications:read.label': 'Zobrazit oznámení', 'oauth.scope.notifications:read.label': 'Zobrazit oznámení',
'oauth.scope.notifications:read.description': 'oauth.scope.notifications:read.description': 'Číst oznámení v aplikaci a počty nepřečtených',
'Číst oznámení v aplikaci a počty nepřečtených',
'oauth.scope.notifications:write.label': 'Spravovat oznámení', 'oauth.scope.notifications:write.label': 'Spravovat oznámení',
'oauth.scope.notifications:write.description': 'oauth.scope.notifications:write.description': 'Označovat oznámení jako přečtená a reagovat na ně',
'Označovat oznámení jako přečtená a reagovat na ně',
'oauth.scope.vacay:read.label': 'Zobrazit plány dovolené', 'oauth.scope.vacay:read.label': 'Zobrazit plány dovolené',
'oauth.scope.vacay:read.description': 'oauth.scope.vacay:read.description': 'Číst data plánování dovolené, záznamy a statistiky',
'Číst data plánování dovolené, záznamy a statistiky',
'oauth.scope.vacay:write.label': 'Spravovat plány dovolené', 'oauth.scope.vacay:write.label': 'Spravovat plány dovolené',
'oauth.scope.vacay:write.description': 'oauth.scope.vacay:write.description': 'Vytvářet a spravovat záznamy dovolené, svátky a týmové plány',
'Vytvářet a spravovat záznamy dovolené, svátky a týmové plány',
'oauth.scope.geo:read.label': 'Mapy a geokódování', 'oauth.scope.geo:read.label': 'Mapy a geokódování',
'oauth.scope.geo:read.description': 'oauth.scope.geo:read.description': 'Vyhledávat místa, řešit URL map a zpětně geokódovat souřadnice',
'Vyhledávat místa, řešit URL map a zpětně geokódovat souřadnice',
'oauth.scope.weather:read.label': 'Předpovědi počasí', 'oauth.scope.weather:read.label': 'Předpovědi počasí',
'oauth.scope.weather:read.description': 'oauth.scope.weather:read.description': 'Získávat předpovědi počasí pro místa a data výletu',
'Získávat předpovědi počasí pro místa a data výletu',
'oauth.scope.journey:read.label': 'Zobrazit cestovní deníky', 'oauth.scope.journey:read.label': 'Zobrazit cestovní deníky',
'oauth.scope.journey:read.description': 'oauth.scope.journey:read.description': 'Číst cestovní deníky, záznamy a seznam přispěvatelů',
'Číst cestovní deníky, záznamy a seznam přispěvatelů',
'oauth.scope.journey:write.label': 'Spravovat cestovní deníky', 'oauth.scope.journey:write.label': 'Spravovat cestovní deníky',
'oauth.scope.journey:write.description': 'oauth.scope.journey:write.description': 'Vytvářet, aktualizovat a mazat cestovní deníky a jejich záznamy',
'Vytvářet, aktualizovat a mazat cestovní deníky a jejich záznamy',
'oauth.scope.journey:share.label': 'Spravovat odkazy na cestovní deníky', 'oauth.scope.journey:share.label': 'Spravovat odkazy na cestovní deníky',
'oauth.scope.journey:share.description': 'oauth.scope.journey:share.description': 'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy na cestovní deníky',
'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy na cestovní deníky',
'oauth.authorize.authorizing': 'Authorizing…', // en-fallback 'oauth.authorize.authorizing': 'Authorizing…', // en-fallback
'oauth.authorize.loading': 'Loading…', // en-fallback 'oauth.authorize.loading': 'Loading…', // en-fallback
'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback 'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback
'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback 'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback
'oauth.authorize.loginDescription': 'oauth.authorize.loginDescription': '{client} wants access to your TREK account. Please sign in first.', // en-fallback
'{client} wants access to your TREK account. Please sign in first.', // en-fallback
'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback 'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback
'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback 'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback
'oauth.authorize.requestDescription': 'oauth.authorize.requestDescription': 'This application is requesting access to your TREK account.', // en-fallback
'This application is requesting access to your TREK account.', // en-fallback 'oauth.authorize.trustNote': 'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.trustNote':
'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.selectScope': 'Select at least one scope', // en-fallback 'oauth.authorize.selectScope': 'Select at least one scope', // en-fallback
'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // en-fallback 'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // en-fallback
'oauth.authorize.approveManyScopes': 'Approve ({count} scopes)', // en-fallback 'oauth.authorize.approveManyScopes': 'Approve ({count} scopes)', // en-fallback
@@ -114,9 +85,7 @@ const oauth: TranslationStrings = {
'oauth.authorize.choosePermissions': 'Choose which permissions to grant', // en-fallback 'oauth.authorize.choosePermissions': 'Choose which permissions to grant', // en-fallback
'oauth.authorize.permissionsRequested': 'Permissions requested', // en-fallback 'oauth.authorize.permissionsRequested': 'Permissions requested', // en-fallback
'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback 'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback
'oauth.authorize.alwaysTool.listTrips': 'oauth.authorize.alwaysTool.listTrips': 'List your trips so the AI can discover trip IDs', // en-fallback
'List your trips so the AI can discover trip IDs', // en-fallback 'oauth.authorize.alwaysTool.getTripSummary': 'Read a trip overview needed to use any other tool', // en-fallback
'oauth.authorize.alwaysTool.getTripSummary':
'Read a trip overview needed to use any other tool', // en-fallback
}; };
export default oauth; export default oauth;
+2 -4
View File
@@ -51,10 +51,8 @@ const packing: TranslationStrings = {
'packing.bagName': 'Název zavazadla...', 'packing.bagName': 'Název zavazadla...',
'packing.addBag': 'Přidat zavazadlo', 'packing.addBag': 'Přidat zavazadlo',
'packing.changeCategory': 'Změnit kategorii', 'packing.changeCategory': 'Změnit kategorii',
'packing.confirm.clearChecked': 'packing.confirm.clearChecked': 'Opravdu chcete odstranit {count} zabalených položek?',
'Opravdu chcete odstranit {count} zabalených položek?', 'packing.confirm.deleteCat': 'Opravdu chcete smazat kategorii „{name}" s {count} položkami?',
'packing.confirm.deleteCat':
'Opravdu chcete smazat kategorii „{name}" s {count} položkami?',
'packing.defaultCategory': 'Ostatní', 'packing.defaultCategory': 'Ostatní',
'packing.toast.saveError': 'Uložení se nezdařilo', 'packing.toast.saveError': 'Uložení se nezdařilo',
'packing.toast.deleteError': 'Smazání se nezdařilo', 'packing.toast.deleteError': 'Smazání se nezdařilo',
+8 -16
View File
@@ -32,28 +32,20 @@ const perm: TranslationStrings = {
'perm.action.collab_edit': 'Spolupráce (poznámky, hlasování, chat)', 'perm.action.collab_edit': 'Spolupráce (poznámky, hlasování, chat)',
'perm.action.share_manage': 'Spravovat odkazy ke sdílení', 'perm.action.share_manage': 'Spravovat odkazy ke sdílení',
'perm.actionHint.trip_create': 'Kdo může vytvářet nové výlety', 'perm.actionHint.trip_create': 'Kdo může vytvářet nové výlety',
'perm.actionHint.trip_edit': 'perm.actionHint.trip_edit': 'Kdo může měnit název, data, popis a měnu výletu',
'Kdo může měnit název, data, popis a měnu výletu',
'perm.actionHint.trip_delete': 'Kdo může trvale smazat výlet', 'perm.actionHint.trip_delete': 'Kdo může trvale smazat výlet',
'perm.actionHint.trip_archive': 'Kdo může archivovat nebo odarchivovat výlet', 'perm.actionHint.trip_archive': 'Kdo může archivovat nebo odarchivovat výlet',
'perm.actionHint.trip_cover_upload': 'perm.actionHint.trip_cover_upload': 'Kdo může nahrát nebo změnit titulní obrázek',
'Kdo může nahrát nebo změnit titulní obrázek',
'perm.actionHint.member_manage': 'Kdo může pozvat nebo odebrat členy výletu', 'perm.actionHint.member_manage': 'Kdo může pozvat nebo odebrat členy výletu',
'perm.actionHint.file_upload': 'Kdo může nahrávat soubory k výletu', 'perm.actionHint.file_upload': 'Kdo může nahrávat soubory k výletu',
'perm.actionHint.file_edit': 'Kdo může upravovat popisy a odkazy souborů', 'perm.actionHint.file_edit': 'Kdo může upravovat popisy a odkazy souborů',
'perm.actionHint.file_delete': 'perm.actionHint.file_delete': 'Kdo může přesunout soubory do koše nebo je trvale smazat',
'Kdo může přesunout soubory do koše nebo je trvale smazat',
'perm.actionHint.place_edit': 'Kdo může přidávat, upravovat nebo mazat místa', 'perm.actionHint.place_edit': 'Kdo může přidávat, upravovat nebo mazat místa',
'perm.actionHint.day_edit': 'perm.actionHint.day_edit': 'Kdo může upravovat dny, poznámky ke dnům a přiřazení míst',
'Kdo může upravovat dny, poznámky ke dnům a přiřazení míst', 'perm.actionHint.reservation_edit': 'Kdo může vytvářet, upravovat nebo mazat rezervace',
'perm.actionHint.reservation_edit': 'perm.actionHint.budget_edit': 'Kdo může vytvářet, upravovat nebo mazat položky rozpočtu',
'Kdo může vytvářet, upravovat nebo mazat rezervace',
'perm.actionHint.budget_edit':
'Kdo může vytvářet, upravovat nebo mazat položky rozpočtu',
'perm.actionHint.packing_edit': 'Kdo může spravovat položky balení a tašky', 'perm.actionHint.packing_edit': 'Kdo může spravovat položky balení a tašky',
'perm.actionHint.collab_edit': 'perm.actionHint.collab_edit': 'Kdo může vytvářet poznámky, hlasování a posílat zprávy',
'Kdo může vytvářet poznámky, hlasování a posílat zprávy', 'perm.actionHint.share_manage': 'Kdo může vytvářet nebo mazat veřejné odkazy ke sdílení',
'perm.actionHint.share_manage':
'Kdo může vytvářet nebo mazat veřejné odkazy ke sdílení',
}; };
export default perm; export default perm;
+7 -14
View File
@@ -6,13 +6,10 @@ const places: TranslationStrings = {
'places.sidebarDrop': 'Pusťte pro import', 'places.sidebarDrop': 'Pusťte pro import',
'places.importFileHint': 'places.importFileHint':
'Importujte soubory .gpx, .kml nebo .kmz z nástrojů jako Google My Maps, Google Earth nebo GPS tracker.', 'Importujte soubory .gpx, .kml nebo .kmz z nástrojů jako Google My Maps, Google Earth nebo GPS tracker.',
'places.importFileDropHere': 'places.importFileDropHere': 'Klikněte pro výběr souboru nebo jej přetáhněte sem',
'Klikněte pro výběr souboru nebo jej přetáhněte sem',
'places.importFileDropActive': 'Přetáhněte soubor pro výběr', 'places.importFileDropActive': 'Přetáhněte soubor pro výběr',
'places.importFileUnsupported': 'places.importFileUnsupported': 'Nepodporovaný typ souboru. Použijte .gpx, .kml nebo .kmz.',
'Nepodporovaný typ souboru. Použijte .gpx, .kml nebo .kmz.', 'places.importFileTooLarge': 'Soubor je příliš velký. Maximální velikost nahrání je {maxMb} MB.',
'places.importFileTooLarge':
'Soubor je příliš velký. Maximální velikost nahrání je {maxMb} MB.',
'places.importFileError': 'Import se nezdařil', 'places.importFileError': 'Import se nezdařil',
'places.importAllSkipped': 'Všechna místa již byla v cestě.', 'places.importAllSkipped': 'Všechna místa již byla v cestě.',
'places.gpxImported': '{count} míst importováno z GPX', 'places.gpxImported': '{count} míst importováno z GPX',
@@ -30,16 +27,13 @@ const places: TranslationStrings = {
'places.kmlKmzImported': 'Importováno {count} míst z KMZ/KML', 'places.kmlKmzImported': 'Importováno {count} míst z KMZ/KML',
'places.urlResolved': 'Místo importováno z URL', 'places.urlResolved': 'Místo importováno z URL',
'places.importList': 'Import seznamu', 'places.importList': 'Import seznamu',
'places.kmlKmzSummaryValues': 'places.kmlKmzSummaryValues': 'Placemarks: {total} • Importováno: {created} • Přeskočeno: {skipped}',
'Placemarks: {total} • Importováno: {created} • Přeskočeno: {skipped}',
'places.importGoogleList': 'Google Seznam', 'places.importGoogleList': 'Google Seznam',
'places.importNaverList': 'Naver Seznam', 'places.importNaverList': 'Naver Seznam',
'places.googleListHint': 'places.googleListHint': 'Vložte sdílený odkaz na seznam Google Maps pro import všech míst.',
'Vložte sdílený odkaz na seznam Google Maps pro import všech míst.',
'places.googleListImported': '{count} míst importováno ze seznamu "{list}"', 'places.googleListImported': '{count} míst importováno ze seznamu "{list}"',
'places.googleListError': 'Import seznamu Google Maps se nezdařil', 'places.googleListError': 'Import seznamu Google Maps se nezdařil',
'places.naverListHint': 'places.naverListHint': 'Vložte sdílený odkaz na seznam Naver Maps pro import všech míst.',
'Vložte sdílený odkaz na seznam Naver Maps pro import všech míst.',
'places.naverListImported': '{count} míst importováno ze seznamu "{list}"', 'places.naverListImported': '{count} míst importováno ze seznamu "{list}"',
'places.naverListError': 'Import seznamu Naver Maps se nezdařil', 'places.naverListError': 'Import seznamu Naver Maps se nezdařil',
'places.viewDetails': 'Zobrazit detaily', 'places.viewDetails': 'Zobrazit detaily',
@@ -76,8 +70,7 @@ const places: TranslationStrings = {
'places.formNotes': 'Poznámky', 'places.formNotes': 'Poznámky',
'places.formNotesPlaceholder': 'Osobní poznámky...', 'places.formNotesPlaceholder': 'Osobní poznámky...',
'places.formReservation': 'Rezervace', 'places.formReservation': 'Rezervace',
'places.reservationNotesPlaceholder': 'places.reservationNotesPlaceholder': 'Poznámky k rezervaci, potvrzovací kód...',
'Poznámky k rezervaci, potvrzovací kód...',
'places.mapsSearchPlaceholder': 'Hledat místa...', 'places.mapsSearchPlaceholder': 'Hledat místa...',
'places.mapsSearchError': 'Hledání místa se nezdařilo.', 'places.mapsSearchError': 'Hledání místa se nezdařilo.',
'places.loadingDetails': 'Načítání podrobností místa…', 'places.loadingDetails': 'Načítání podrobností místa…',

Some files were not shown because too many files have changed in this diff Show More