mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(costs): rework the cost panel UX wise and apply prettier on the shared package
This commit is contained in:
@@ -595,6 +595,7 @@ export const budgetApi = {
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
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 { useAuthStore } from '../../store/authStore'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
@@ -39,6 +39,12 @@ interface SettlementData {
|
||||
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 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 [filter, setFilter] = useState<'all' | 'mine' | 'owed'>('all')
|
||||
const [search, setSearch] = useState('')
|
||||
const [histOpen, setHistOpen] = useState(false)
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<BudgetItem | null>(null)
|
||||
const [editingSettlement, setEditingSettlement] = useState<Settlement | null>(null)
|
||||
const [addingPayment, setAddingPayment] = useState(false)
|
||||
|
||||
const people = tripMembers
|
||||
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
|
||||
}, [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 groups: { day: string; items: BudgetItem[] }[] = []
|
||||
const labelOf = (e: BudgetItem) => {
|
||||
if (!e.expense_date) return t('costs.noDate')
|
||||
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 entries: LedgerEntry[] = [
|
||||
...filtered.map(e => ({ kind: 'expense' as const, date: e.expense_date || '', e })),
|
||||
...filteredSettlements.map(s => ({ kind: 'payment' as const, date: (s.created_at || '').slice(0, 10), s })),
|
||||
]
|
||||
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 || ''))
|
||||
for (const e of sorted) {
|
||||
const day = labelOf(e)
|
||||
// Newest day first; within a day, expenses before payments (insertion order).
|
||||
const sorted = entries.slice().sort((a, b) => (b.date || '').localeCompare(a.date || ''))
|
||||
const groups: { day: string; entries: LedgerEntry[] }[] = []
|
||||
for (const en of sorted) {
|
||||
const day = labelOf(en.date)
|
||||
let g = groups.find(x => x.day === day)
|
||||
if (!g) { g = { day, items: [] }; groups.push(g) }
|
||||
g.items.push(e)
|
||||
if (!g) { g = { day, entries: [] }; groups.push(g) }
|
||||
g.entries.push(en)
|
||||
}
|
||||
return groups
|
||||
}, [filtered, locale, t])
|
||||
}, [filtered, filteredSettlements, locale, t])
|
||||
|
||||
// ── settle actions ──────────────────────────────────────────────────────
|
||||
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')}
|
||||
</div>
|
||||
) : 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 (
|
||||
<div key={g.day} style={{ marginBottom: 22 }}>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
@@ -300,11 +325,13 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
<div className={cardCls} style={{ borderRadius: 22, padding: '22px 24px' }}>
|
||||
<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>
|
||||
<button disabled={!(settlement?.settlements || []).length} onClick={() => setHistOpen(true)}
|
||||
className="text-content-muted bg-surface-secondary border border-edge disabled:opacity-40"
|
||||
{canEdit && (
|
||||
<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' }}>
|
||||
<History size={13} /> {t('costs.history')}{(settlement?.settlements || []).length ? ` (${settlement!.settlements.length})` : ''}
|
||||
<Plus size={13} /> {t('costs.addPayment')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<SettleFlows />
|
||||
</div>
|
||||
@@ -330,9 +357,11 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
onSaved={() => { setModalOpen(false); loadBudgetItems(tripId); loadSettlement() }} />
|
||||
)}
|
||||
|
||||
<Modal isOpen={histOpen} onClose={() => setHistOpen(false)} title={t('costs.settleHistory')} size="md">
|
||||
<SettleHistory settlements={settlement?.settlements || []} fmt={fmt} Avatar={Avatar} name={personName} onUndo={undoSettlement} canEdit={canEdit} />
|
||||
</Modal>
|
||||
{(editingSettlement || addingPayment) && (
|
||||
<SettlementModal tripId={tripId} people={people} me={me} editing={editingSettlement}
|
||||
onClose={() => { setEditingSettlement(null); setAddingPayment(false) }}
|
||||
onSaved={() => { setEditingSettlement(null); setAddingPayment(false); loadSettlement() }} />
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.costs-root {
|
||||
@@ -438,7 +467,9 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
<div className={cardCls} style={{ borderRadius: 18, padding: 16 }}>
|
||||
<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>
|
||||
<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>
|
||||
<SettleFlows />
|
||||
</div>
|
||||
@@ -458,11 +489,13 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
{dayGroups.length === 0
|
||||
? <div className="text-content-faint" style={{ textAlign: 'center', padding: '36px 16px', fontSize: 13 }}>{search ? t('costs.noMatch') : t('costs.emptyText')}</div>
|
||||
: 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 (
|
||||
<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 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>
|
||||
)
|
||||
})}
|
||||
@@ -490,11 +523,22 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
const cur = curOf(e)
|
||||
const payers = (e.payers || []).filter(p => p.amount > 0)
|
||||
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 (
|
||||
<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>
|
||||
<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 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 5 }}>
|
||||
{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={{ textAlign: 'right', whiteSpace: 'nowrap' }}>
|
||||
<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' }}>
|
||||
{net > 0 ? t('costs.youLent', { amount: fmt(net) }) : t('costs.youBorrowed', { amount: fmt(-net) })}
|
||||
</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'] }) {
|
||||
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)))
|
||||
@@ -633,31 +703,62 @@ function FlowPills({ ids, lead, Avatar, name }: { ids: number[]; lead: string; A
|
||||
)
|
||||
}
|
||||
|
||||
function SettleHistory({ settlements, fmt, Avatar, name, onUndo, canEdit }: {
|
||||
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
|
||||
// Add or edit a settle-up payment (from / to / amount). Reachable inline from the
|
||||
// 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()
|
||||
if (settlements.length === 0) return <div className="text-content-faint" style={{ textAlign: 'center', padding: 30, fontSize: 13 }}>{t('costs.noSettlements')}</div>
|
||||
const total = settlements.reduce((a, s) => a + s.amount, 0)
|
||||
const toast = useToast()
|
||||
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 (
|
||||
<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 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 }}>
|
||||
<span>{t('costs.paymentsSettled', { count: settlements.length })}</span><span>{fmt(total)}</span>
|
||||
<label className={labelCls}>{t('costs.from')}</label>
|
||||
<CustomSelect value={fromId} onChange={v => setFromId(String(v))} options={opts} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{settlements.map(s => (
|
||||
<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 }}>
|
||||
<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>
|
||||
<label className={labelCls}>{t('costs.to')}</label>
|
||||
<CustomSelect value={toId} onChange={v => setToId(String(v))} options={opts} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span className="text-content" style={{ fontSize: 14, fontWeight: 600 }}>{fmt(s.amount)}</span>
|
||||
{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>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<label className={labelCls}>{t('costs.amount')}</label>
|
||||
<input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={amount}
|
||||
onChange={e => setAmount(e.target.value)} className={inputCls} style={{ borderRadius: 10, padding: '11px 13px', fontSize: 14, outline: 'none', fontWeight: 600 }} />
|
||||
</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 [currency, setCurrency] = useState((editing?.currency || base).toUpperCase())
|
||||
const [day, setDay] = useState(editing?.expense_date || new Date().toISOString().slice(0, 10))
|
||||
const [payers, setPayers] = useState<Record<number, string>>(() => {
|
||||
const m: Record<number, string> = {}
|
||||
for (const p of editing?.payers || []) m[p.user_id] = String(p.amount)
|
||||
return m
|
||||
})
|
||||
// Standalone total for "recorded amount, nobody has paid yet" expenses (created
|
||||
// from a booking, or pre-rework items). Used only while no per-person amount is
|
||||
// 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) : ''
|
||||
// One participant list: a person is "in" the split and may have paid an amount.
|
||||
// Entering the total auto-distributes it equally across the non-pinned participants;
|
||||
// touching an amount pins it and the rest rebalance so the paid amounts always sum
|
||||
// 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).
|
||||
const [total, setTotal] = useState<string>(() => {
|
||||
if (editing) return editing.total_price ? String(editing.total_price) : ''
|
||||
if (prefill?.amount != null) return String(prefill.amount)
|
||||
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)))
|
||||
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 payersTotal = Object.values(payers).reduce((a, v) => a + (parseFloat(v) || 0), 0)
|
||||
const hasPayers = payersTotal > 0
|
||||
const total = hasPayers ? payersTotal : (parseFloat(amount) || 0)
|
||||
const each = split.size > 0 ? total / split.size : 0
|
||||
const valid = name.trim().length > 0 && total > 0 && (hasPayers ? split.size > 0 : true)
|
||||
const totalNum = parseFloat(total) || 0
|
||||
const paidSum = round2([...participants].reduce((a, id) => a + (parseFloat(paid[id]) || 0), 0))
|
||||
const paidEntered = paidSum > 0
|
||||
const balanced = Math.abs(paidSum - totalNum) < 0.01
|
||||
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 () => {
|
||||
if (!valid) return
|
||||
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 = {
|
||||
name: name.trim(), category: cat,
|
||||
// Store the actual currency the amounts were entered in; conversion to the
|
||||
// viewer's display currency happens live (real rates), no manual rate.
|
||||
currency,
|
||||
payers: payerList, member_ids: [...split],
|
||||
payers: payerList, member_ids: [...participants],
|
||||
expense_date: day || null,
|
||||
// No per-person amounts: record the typed total directly (the server keeps
|
||||
// it instead of deriving 0 from the empty payer list).
|
||||
...(payerList.length === 0 ? { total_price: parseFloat(amount) || 0 } : {}),
|
||||
// Always record the entered total: the server keeps it as-is for an unfinished
|
||||
// expense (no payers) and otherwise re-derives it from the payer sum (== total).
|
||||
total_price: totalNum,
|
||||
// Link a freshly-created expense to its booking (create-from-booking flow).
|
||||
...(!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>
|
||||
<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>
|
||||
{hasPayers ? (
|
||||
<span className="text-content" style={{ flex: 1, fontSize: 15, fontWeight: 600, paddingLeft: 6 }}>{payersTotal.toFixed(2)}</span>
|
||||
) : (
|
||||
<input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={amount}
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
<input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={total}
|
||||
onChange={e => onTotalChange(e.target.value)}
|
||||
className="text-content" style={{ flex: 1, border: 0, background: 'none', outline: 'none', fontSize: 15, fontWeight: 600, paddingLeft: 6, width: '100%' }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{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' }}>
|
||||
<span>{formatMoney(total, currency, locale)}</span>
|
||||
<span>{formatMoney(totalNum, currency, locale)}</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>
|
||||
</div>
|
||||
)}
|
||||
@@ -801,39 +943,37 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo
|
||||
<div>
|
||||
<label className={labelCls}>{t('costs.whoPaid')}</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
{people.map(p => (
|
||||
<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 }}>
|
||||
<span className="text-content" style={{ fontSize: 14, fontWeight: 500 }}>{p.id === me ? t('costs.you') : p.username}</span>
|
||||
{people.map((p, idx) => {
|
||||
const on = participants.has(p.id)
|
||||
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' }}>
|
||||
<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] || ''}
|
||||
onChange={e => setPayers(prev => ({ ...prev, [p.id]: e.target.value }))}
|
||||
<input type="number" inputMode="decimal" min="0" step="0.01" placeholder="0.00" value={paid[p.id] || ''}
|
||||
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' }} />
|
||||
</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>
|
||||
<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 className="text-content-faint" style={{ marginTop: 10, fontSize: 12.5 }}>
|
||||
{split.size === 0 ? t('costs.pickSomeone') : t('costs.splitSummary', { count: split.size, amount: sym(currency) + each.toFixed(2) })}
|
||||
<div style={{ marginTop: 10, fontSize: 12.5, display: 'flex', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
|
||||
<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>
|
||||
|
||||
@@ -94,6 +94,31 @@ export class BudgetController {
|
||||
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')
|
||||
deleteSettlement(
|
||||
@CurrentUser() user: User,
|
||||
|
||||
@@ -73,6 +73,10 @@ export class BudgetService {
|
||||
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 {
|
||||
return svc.deleteSettlement(id, tripId);
|
||||
}
|
||||
|
||||
@@ -385,11 +385,18 @@ export function calculateSettlement(
|
||||
}
|
||||
|
||||
// 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 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) {
|
||||
if (balances[s.from_user_id]) balances[s.from_user_id].balance += s.amount;
|
||||
if (balances[s.to_user_id]) balances[s.to_user_id].balance -= s.amount;
|
||||
ensureSettled(s.from_user_id, s.from_username, s.from_avatar_url).balance += s.amount;
|
||||
ensureSettled(s.to_user_id, s.to_username, s.to_avatar_url).balance -= s.amount;
|
||||
}
|
||||
|
||||
// Calculate optimized payment flows (greedy algorithm)
|
||||
@@ -461,6 +468,19 @@ export function createSettlement(
|
||||
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 {
|
||||
const row = db.prepare('SELECT id FROM budget_settlements WHERE id = ? AND trip_id = ?').get(id, tripId);
|
||||
if (!row) return false;
|
||||
|
||||
@@ -31,6 +31,7 @@ const { svc } = vi.hoisted(() => ({
|
||||
verifyTripAccess: vi.fn(), listBudgetItems: vi.fn(), createBudgetItem: vi.fn(), updateBudgetItem: vi.fn(),
|
||||
deleteBudgetItem: vi.fn(), updateMembers: vi.fn(), toggleMemberPaid: vi.fn(), getPerPersonSummary: 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);
|
||||
@@ -104,4 +105,18 @@ describe('Budget e2e (real auth guard + temp SQLite)', () => {
|
||||
expect(res.status).toBe(400);
|
||||
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(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 /', () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ const mockDb = vi.hoisted(() => {
|
||||
|
||||
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';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -189,4 +189,60 @@ describe('calculateSettlement', () => {
|
||||
expect(result.flows).toHaveLength(1);
|
||||
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,4 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"plugins": [
|
||||
|
||||
@@ -16,16 +16,9 @@ describe('adminUserCreateRequestSchema', () => {
|
||||
role: 'admin',
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
adminUserCreateRequestSchema.safeParse({ password: 'p' }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', role: 'root' })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true);
|
||||
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,
|
||||
).toBe(true);
|
||||
expect(adminInviteCreateRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(
|
||||
adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success,
|
||||
).toBe(false);
|
||||
expect(adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adminFeatureToggleRequestSchema', () => {
|
||||
it('requires a boolean enabled', () => {
|
||||
expect(
|
||||
adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success,
|
||||
).toBe(false);
|
||||
expect(adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success).toBe(true);
|
||||
expect(adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,29 +14,21 @@ export const adminUserCreateRequestSchema = z.object({
|
||||
username: z.string().optional(),
|
||||
role: z.enum(['user', 'admin']).optional(),
|
||||
});
|
||||
export type AdminUserCreateRequest = z.infer<
|
||||
typeof adminUserCreateRequestSchema
|
||||
>;
|
||||
export type AdminUserCreateRequest = z.infer<typeof adminUserCreateRequestSchema>;
|
||||
|
||||
export const adminPermissionsRequestSchema = z.object({
|
||||
permissions: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
export type AdminPermissionsRequest = z.infer<
|
||||
typeof adminPermissionsRequestSchema
|
||||
>;
|
||||
export type AdminPermissionsRequest = z.infer<typeof adminPermissionsRequestSchema>;
|
||||
|
||||
export const adminInviteCreateRequestSchema = z.object({
|
||||
max_uses: z.number().optional(),
|
||||
expires_in_days: z.number().optional(),
|
||||
role: z.enum(['user', 'admin']).optional(),
|
||||
});
|
||||
export type AdminInviteCreateRequest = z.infer<
|
||||
typeof adminInviteCreateRequestSchema
|
||||
>;
|
||||
export type AdminInviteCreateRequest = z.infer<typeof adminInviteCreateRequestSchema>;
|
||||
|
||||
export const adminFeatureToggleRequestSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
export type AdminFeatureToggleRequest = z.infer<
|
||||
typeof adminFeatureToggleRequestSchema
|
||||
>;
|
||||
export type AdminFeatureToggleRequest = z.infer<typeof adminFeatureToggleRequestSchema>;
|
||||
|
||||
@@ -8,38 +8,23 @@ import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('assignmentCreateRequestSchema', () => {
|
||||
it('requires a place_id; notes optional/nullable', () => {
|
||||
expect(
|
||||
assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(true);
|
||||
expect(assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null }).success).toBe(true);
|
||||
expect(assignmentCreateRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignmentMoveRequestSchema', () => {
|
||||
it('requires new_day_id; order_index optional', () => {
|
||||
expect(
|
||||
assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success).toBe(true);
|
||||
expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 }).success).toBe(true);
|
||||
expect(assignmentMoveRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignmentParticipantsRequestSchema', () => {
|
||||
it('requires a numeric user_ids array', () => {
|
||||
expect(
|
||||
assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success,
|
||||
).toBe(false);
|
||||
expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
|
||||
expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,16 +49,12 @@ export const assignmentCreateRequestSchema = z.object({
|
||||
place_id: z.union([z.number(), z.string()]),
|
||||
notes: z.string().nullable().optional(),
|
||||
});
|
||||
export type AssignmentCreateRequest = z.infer<
|
||||
typeof assignmentCreateRequestSchema
|
||||
>;
|
||||
export type AssignmentCreateRequest = z.infer<typeof assignmentCreateRequestSchema>;
|
||||
|
||||
export const assignmentReorderRequestSchema = z.object({
|
||||
orderedIds: z.array(z.number()),
|
||||
});
|
||||
export type AssignmentReorderRequest = z.infer<
|
||||
typeof assignmentReorderRequestSchema
|
||||
>;
|
||||
export type AssignmentReorderRequest = z.infer<typeof assignmentReorderRequestSchema>;
|
||||
|
||||
export const assignmentMoveRequestSchema = z.object({
|
||||
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({
|
||||
user_ids: z.array(z.number()),
|
||||
});
|
||||
export type AssignmentParticipantsRequest = z.infer<
|
||||
typeof assignmentParticipantsRequestSchema
|
||||
>;
|
||||
export type AssignmentParticipantsRequest = z.infer<typeof assignmentParticipantsRequestSchema>;
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import {
|
||||
markRegionRequestSchema,
|
||||
createBucketItemRequestSchema,
|
||||
regionGeoSchema,
|
||||
} from './atlas.schema';
|
||||
import { markRegionRequestSchema, createBucketItemRequestSchema, regionGeoSchema } from './atlas.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('markRegionRequestSchema', () => {
|
||||
it('requires both name and country_code', () => {
|
||||
expect(
|
||||
markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' }).success).toBe(true);
|
||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBucketItemRequestSchema', () => {
|
||||
it('requires a name; coordinates and metadata optional/nullable', () => {
|
||||
expect(
|
||||
createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success,
|
||||
).toBe(true);
|
||||
expect(createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success).toBe(true);
|
||||
expect(
|
||||
createBucketItemRequestSchema.safeParse({
|
||||
name: 'Tokyo',
|
||||
@@ -37,18 +26,13 @@ describe('createBucketItemRequestSchema', () => {
|
||||
|
||||
describe('regionGeoSchema', () => {
|
||||
it('accepts a FeatureCollection with opaque features', () => {
|
||||
expect(
|
||||
regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] }).success).toBe(true);
|
||||
expect(
|
||||
regionGeoSchema.safeParse({
|
||||
type: 'FeatureCollection',
|
||||
features: [{ anything: true }],
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
regionGeoSchema.safeParse({ type: 'Other', features: [] }).success,
|
||||
).toBe(false);
|
||||
expect(regionGeoSchema.safeParse({ type: 'Other', features: [] }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,9 +29,7 @@ export const createBucketItemRequestSchema = z.object({
|
||||
notes: z.string().nullable().optional(),
|
||||
target_date: z.string().nullable().optional(),
|
||||
});
|
||||
export type CreateBucketItemRequest = z.infer<
|
||||
typeof createBucketItemRequestSchema
|
||||
>;
|
||||
export type CreateBucketItemRequest = z.infer<typeof createBucketItemRequestSchema>;
|
||||
|
||||
export const updateBucketItemRequestSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
@@ -41,9 +39,7 @@ export const updateBucketItemRequestSchema = z.object({
|
||||
country_code: z.string().nullable().optional(),
|
||||
target_date: z.string().nullable().optional(),
|
||||
});
|
||||
export type UpdateBucketItemRequest = z.infer<
|
||||
typeof updateBucketItemRequestSchema
|
||||
>;
|
||||
export type UpdateBucketItemRequest = z.infer<typeof updateBucketItemRequestSchema>;
|
||||
|
||||
/** A bucket-list item row (DB-shaped; kept 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).
|
||||
*/
|
||||
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',
|
||||
BA:'Europe',BD:'Asia',BF:'Africa',BH:'Asia',BI:'Africa',BJ:'Africa',BN:'Asia',BO:'South America',
|
||||
BR:'South America',BE:'Europe',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',
|
||||
AF: 'Asia',
|
||||
AL: 'Europe',
|
||||
DZ: 'Africa',
|
||||
AD: 'Europe',
|
||||
AO: 'Africa',
|
||||
AR: 'South America',
|
||||
AM: 'Asia',
|
||||
AU: 'Oceania',
|
||||
AT: 'Europe',
|
||||
AZ: 'Asia',
|
||||
BA: 'Europe',
|
||||
BD: 'Asia',
|
||||
BF: 'Africa',
|
||||
BH: 'Asia',
|
||||
BI: 'Africa',
|
||||
BJ: 'Africa',
|
||||
BN: 'Asia',
|
||||
BO: 'South America',
|
||||
BR: 'South America',
|
||||
BE: 'Europe',
|
||||
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. */
|
||||
|
||||
@@ -13,10 +13,7 @@ import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('registerRequestSchema', () => {
|
||||
it('requires email + password; username/invite optional', () => {
|
||||
expect(
|
||||
registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true);
|
||||
expect(
|
||||
registerRequestSchema.safeParse({
|
||||
email: 'a@b.c',
|
||||
@@ -24,32 +21,21 @@ describe('registerRequestSchema', () => {
|
||||
invite_token: 't',
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loginRequestSchema', () => {
|
||||
it('requires email + password', () => {
|
||||
expect(
|
||||
loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success,
|
||||
).toBe(true);
|
||||
expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true);
|
||||
expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forgot/reset/change password schemas', () => {
|
||||
it('validate their required fields', () => {
|
||||
expect(
|
||||
forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true);
|
||||
expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' }).success).toBe(true);
|
||||
expect(
|
||||
resetPasswordRequestSchema.safeParse({
|
||||
token: 't',
|
||||
@@ -57,36 +43,23 @@ describe('forgot/reset/change password schemas', () => {
|
||||
mfa_code: '123456',
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success,
|
||||
).toBe(false);
|
||||
expect(resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success).toBe(false);
|
||||
expect(
|
||||
changePasswordRequestSchema.safeParse({
|
||||
current_password: 'a',
|
||||
new_password: 'b',
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
changePasswordRequestSchema.safeParse({ new_password: 'b' }).success,
|
||||
).toBe(false);
|
||||
expect(changePasswordRequestSchema.safeParse({ new_password: 'b' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mfa + mcp-token schemas', () => {
|
||||
it('validate their fields', () => {
|
||||
expect(
|
||||
mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' })
|
||||
.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(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' }).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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,16 +11,11 @@ describe('autoBackupSettingsRequestSchema', () => {
|
||||
keep_days: 7,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' }).success).toBe(true);
|
||||
expect(autoBackupSettingsRequestSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a non-boolean enabled', () => {
|
||||
expect(
|
||||
autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success,
|
||||
).toBe(false);
|
||||
expect(autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,4 @@ export const autoBackupSettingsRequestSchema = z
|
||||
time: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
export type AutoBackupSettingsRequest = z.infer<
|
||||
typeof autoBackupSettingsRequestSchema
|
||||
>;
|
||||
export type AutoBackupSettingsRequest = z.infer<typeof autoBackupSettingsRequestSchema>;
|
||||
|
||||
@@ -9,9 +9,7 @@ import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('budgetCreateItemRequestSchema', () => {
|
||||
it('requires a name; money/meta fields optional + nullable', () => {
|
||||
expect(
|
||||
budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success,
|
||||
).toBe(true);
|
||||
expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success).toBe(true);
|
||||
expect(
|
||||
budgetCreateItemRequestSchema.safeParse({
|
||||
name: 'Hotel',
|
||||
@@ -25,34 +23,21 @@ describe('budgetCreateItemRequestSchema', () => {
|
||||
|
||||
describe('budgetUpdateMembersRequestSchema', () => {
|
||||
it('requires a numeric user_ids array', () => {
|
||||
expect(
|
||||
budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success,
|
||||
).toBe(false);
|
||||
expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
|
||||
expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('budgetToggleMemberPaidRequestSchema', () => {
|
||||
it('requires a boolean paid', () => {
|
||||
expect(
|
||||
budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success,
|
||||
).toBe(false);
|
||||
expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success).toBe(true);
|
||||
expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('budgetReorderItemsRequestSchema', () => {
|
||||
it('requires numeric ids', () => {
|
||||
expect(
|
||||
budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success,
|
||||
).toBe(false);
|
||||
expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true);
|
||||
expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,9 +145,7 @@ export const budgetCreateItemRequestSchema = z.object({
|
||||
// "add expense" flow). The server stores it on budget_items.reservation_id.
|
||||
reservation_id: z.number().optional(),
|
||||
});
|
||||
export type BudgetCreateItemRequest = z.infer<
|
||||
typeof budgetCreateItemRequestSchema
|
||||
>;
|
||||
export type BudgetCreateItemRequest = z.infer<typeof budgetCreateItemRequestSchema>;
|
||||
|
||||
/** Update accepts the same fields plus total_price changes; all optional. */
|
||||
export const budgetUpdateItemRequestSchema = z.object({
|
||||
@@ -163,17 +161,13 @@ export const budgetUpdateItemRequestSchema = z.object({
|
||||
note: z.string().nullable().optional(),
|
||||
expense_date: z.string().nullable().optional(),
|
||||
});
|
||||
export type BudgetUpdateItemRequest = z.infer<
|
||||
typeof budgetUpdateItemRequestSchema
|
||||
>;
|
||||
export type BudgetUpdateItemRequest = z.infer<typeof budgetUpdateItemRequestSchema>;
|
||||
|
||||
/** Replace the explicit payers of an expense (amounts in expense currency). */
|
||||
export const budgetUpdatePayersRequestSchema = z.object({
|
||||
payers: z.array(payerInputSchema),
|
||||
});
|
||||
export type BudgetUpdatePayersRequest = z.infer<
|
||||
typeof budgetUpdatePayersRequestSchema
|
||||
>;
|
||||
export type BudgetUpdatePayersRequest = z.infer<typeof budgetUpdatePayersRequestSchema>;
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
amount: z.number(),
|
||||
});
|
||||
export type BudgetCreateSettlementRequest = z.infer<
|
||||
typeof budgetCreateSettlementRequestSchema
|
||||
>;
|
||||
export type BudgetCreateSettlementRequest = z.infer<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({
|
||||
user_ids: z.array(z.number()),
|
||||
});
|
||||
export type BudgetUpdateMembersRequest = z.infer<
|
||||
typeof budgetUpdateMembersRequestSchema
|
||||
>;
|
||||
export type BudgetUpdateMembersRequest = z.infer<typeof budgetUpdateMembersRequestSchema>;
|
||||
|
||||
export const budgetToggleMemberPaidRequestSchema = z.object({
|
||||
paid: z.boolean(),
|
||||
});
|
||||
export type BudgetToggleMemberPaidRequest = z.infer<
|
||||
typeof budgetToggleMemberPaidRequestSchema
|
||||
>;
|
||||
export type BudgetToggleMemberPaidRequest = z.infer<typeof budgetToggleMemberPaidRequestSchema>;
|
||||
|
||||
export const budgetReorderItemsRequestSchema = z.object({
|
||||
orderedIds: z.array(z.number()),
|
||||
});
|
||||
export type BudgetReorderItemsRequest = z.infer<
|
||||
typeof budgetReorderItemsRequestSchema
|
||||
>;
|
||||
export type BudgetReorderItemsRequest = z.infer<typeof budgetReorderItemsRequestSchema>;
|
||||
|
||||
export const budgetReorderCategoriesRequestSchema = z.object({
|
||||
orderedCategories: z.array(z.string()),
|
||||
});
|
||||
export type BudgetReorderCategoriesRequest = z.infer<
|
||||
typeof budgetReorderCategoriesRequestSchema
|
||||
>;
|
||||
export type BudgetReorderCategoriesRequest = z.infer<typeof budgetReorderCategoriesRequestSchema>;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
categorySchema,
|
||||
createCategoryRequestSchema,
|
||||
updateCategoryRequestSchema,
|
||||
} from './category.schema';
|
||||
import { categorySchema, createCategoryRequestSchema, updateCategoryRequestSchema } from './category.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
@@ -21,12 +17,8 @@ describe('categorySchema', () => {
|
||||
|
||||
describe('createCategoryRequestSchema', () => {
|
||||
it('requires a non-empty name; colour and icon are optional', () => {
|
||||
expect(
|
||||
createCategoryRequestSchema.safeParse({ name: 'Food' }).success,
|
||||
).toBe(true);
|
||||
expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(createCategoryRequestSchema.safeParse({ name: 'Food' }).success).toBe(true);
|
||||
expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
||||
expect(createCategoryRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -34,8 +26,6 @@ describe('createCategoryRequestSchema', () => {
|
||||
describe('updateCategoryRequestSchema', () => {
|
||||
it('allows every field to be omitted (the service COALESCEs)', () => {
|
||||
expect(updateCategoryRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(
|
||||
updateCategoryRequestSchema.safeParse({ color: '#000' }).success,
|
||||
).toBe(true);
|
||||
expect(updateCategoryRequestSchema.safeParse({ color: '#000' }).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,12 +10,8 @@ import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('collabNoteCreateRequestSchema', () => {
|
||||
it('requires a non-empty title; the rest is optional', () => {
|
||||
expect(
|
||||
collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success,
|
||||
).toBe(true);
|
||||
expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success).toBe(true);
|
||||
expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(false);
|
||||
expect(collabNoteCreateRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -34,50 +30,29 @@ describe('collabPollCreateRequestSchema', () => {
|
||||
options: ['A'],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success,
|
||||
).toBe(false);
|
||||
expect(collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collabPollVoteRequestSchema', () => {
|
||||
it('requires a numeric option_index', () => {
|
||||
expect(
|
||||
collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success,
|
||||
).toBe(false);
|
||||
expect(collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success).toBe(true);
|
||||
expect(collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collabMessageCreateRequestSchema', () => {
|
||||
it('requires text, caps it at 5000, allows a nullable reply_to', () => {
|
||||
expect(
|
||||
collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null })
|
||||
.success,
|
||||
).toBe(true);
|
||||
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);
|
||||
expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }).success).toBe(true);
|
||||
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', () => {
|
||||
it('requires a non-empty emoji', () => {
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(true);
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,9 +18,7 @@ export const collabNoteCreateRequestSchema = z.object({
|
||||
color: z.string().optional(),
|
||||
website: z.string().optional(),
|
||||
});
|
||||
export type CollabNoteCreateRequest = z.infer<
|
||||
typeof collabNoteCreateRequestSchema
|
||||
>;
|
||||
export type CollabNoteCreateRequest = z.infer<typeof collabNoteCreateRequestSchema>;
|
||||
|
||||
export const collabNoteUpdateRequestSchema = z.object({
|
||||
title: z.string().optional(),
|
||||
@@ -30,9 +28,7 @@ export const collabNoteUpdateRequestSchema = z.object({
|
||||
pinned: z.union([z.boolean(), z.number()]).optional(),
|
||||
website: z.string().optional(),
|
||||
});
|
||||
export type CollabNoteUpdateRequest = z.infer<
|
||||
typeof collabNoteUpdateRequestSchema
|
||||
>;
|
||||
export type CollabNoteUpdateRequest = z.infer<typeof collabNoteUpdateRequestSchema>;
|
||||
|
||||
export const collabPollCreateRequestSchema = z.object({
|
||||
question: z.string().min(1),
|
||||
@@ -41,9 +37,7 @@ export const collabPollCreateRequestSchema = z.object({
|
||||
multiple_choice: z.boolean().optional(),
|
||||
deadline: z.string().optional(),
|
||||
});
|
||||
export type CollabPollCreateRequest = z.infer<
|
||||
typeof collabPollCreateRequestSchema
|
||||
>;
|
||||
export type CollabPollCreateRequest = z.infer<typeof collabPollCreateRequestSchema>;
|
||||
|
||||
export const collabPollVoteRequestSchema = z.object({
|
||||
option_index: z.number(),
|
||||
@@ -54,9 +48,7 @@ export const collabMessageCreateRequestSchema = z.object({
|
||||
text: z.string().min(1).max(5000),
|
||||
reply_to: z.number().nullable().optional(),
|
||||
});
|
||||
export type CollabMessageCreateRequest = z.infer<
|
||||
typeof collabMessageCreateRequestSchema
|
||||
>;
|
||||
export type CollabMessageCreateRequest = z.infer<typeof collabMessageCreateRequestSchema>;
|
||||
|
||||
export const collabReactionRequestSchema = z.object({
|
||||
emoji: z.string().min(1),
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { paginationQuerySchema } from './pagination.schema';
|
||||
import {
|
||||
idSchema,
|
||||
idParamSchema,
|
||||
nonEmptyString,
|
||||
isoDateTime,
|
||||
} from './primitives.schema';
|
||||
import { idSchema, idParamSchema, nonEmptyString, isoDateTime } from './primitives.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
@@ -43,8 +38,6 @@ describe('@trek/shared pagination', () => {
|
||||
|
||||
it('enforces bounds', () => {
|
||||
expect(paginationQuerySchema.safeParse({ perPage: 0 }).success).toBe(false);
|
||||
expect(paginationQuerySchema.safeParse({ perPage: 999 }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(paginationQuerySchema.safeParse({ perPage: 999 }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
import {
|
||||
dayCreateRequestSchema,
|
||||
dayNoteCreateRequestSchema,
|
||||
dayNoteUpdateRequestSchema,
|
||||
} from './day.schema';
|
||||
import { dayCreateRequestSchema, dayNoteCreateRequestSchema, dayNoteUpdateRequestSchema } from './day.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('dayCreateRequestSchema', () => {
|
||||
it('accepts an optional date + notes', () => {
|
||||
expect(dayCreateRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(
|
||||
dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' }).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dayNoteCreateRequestSchema', () => {
|
||||
it('requires non-empty text capped at 500, time capped at 250', () => {
|
||||
expect(
|
||||
dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success,
|
||||
).toBe(true);
|
||||
expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success,
|
||||
).toBe(false);
|
||||
expect(dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success).toBe(true);
|
||||
expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe(false);
|
||||
expect(dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false);
|
||||
expect(
|
||||
dayNoteCreateRequestSchema.safeParse({
|
||||
text: 'ok',
|
||||
@@ -39,11 +26,7 @@ describe('dayNoteCreateRequestSchema', () => {
|
||||
describe('dayNoteUpdateRequestSchema', () => {
|
||||
it('allows omitting text and caps the lengths', () => {
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success,
|
||||
).toBe(false);
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(true);
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import {
|
||||
fileUpdateRequestSchema,
|
||||
fileLinkRequestSchema,
|
||||
photoVariantSchema,
|
||||
} from './file.schema';
|
||||
import { fileUpdateRequestSchema, fileLinkRequestSchema, photoVariantSchema } from './file.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('fileUpdateRequestSchema', () => {
|
||||
it('accepts optional metadata, nullable ids, an empty body', () => {
|
||||
expect(
|
||||
fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 }).success).toBe(true);
|
||||
expect(fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' }).success).toBe(true);
|
||||
expect(fileUpdateRequestSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileLinkRequestSchema', () => {
|
||||
it('accepts any subset of reservation/assignment/place ids', () => {
|
||||
expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(true);
|
||||
expect(fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null }).success).toBe(true);
|
||||
expect(fileLinkRequestSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+37
-68
@@ -2,8 +2,7 @@ import type { TranslationStrings } from '../types';
|
||||
|
||||
const admin: TranslationStrings = {
|
||||
'admin.notifications.title': 'الإشعارات',
|
||||
'admin.notifications.hint':
|
||||
'اختر قناة إشعارات واحدة. يمكن تفعيل واحدة فقط في كل مرة.',
|
||||
'admin.notifications.hint': 'اختر قناة إشعارات واحدة. يمكن تفعيل واحدة فقط في كل مرة.',
|
||||
'admin.notifications.none': 'معطّل',
|
||||
'admin.notifications.email': 'البريد الإلكتروني (SMTP)',
|
||||
'admin.ntfy.hint':
|
||||
@@ -16,18 +15,14 @@ const admin: TranslationStrings = {
|
||||
'admin.notifications.testNtfy': 'إرسال Ntfy تجريبي',
|
||||
'admin.notifications.testNtfySuccess': 'تم إرسال Ntfy التجريبي بنجاح',
|
||||
'admin.notifications.testNtfyFailed': 'فشل إرسال Ntfy التجريبي',
|
||||
'admin.notifications.inappPanel.hint':
|
||||
'الإشعارات داخل التطبيق نشطة دائمًا ولا يمكن تعطيلها بشكل عام.',
|
||||
'admin.notifications.inappPanel.hint': 'الإشعارات داخل التطبيق نشطة دائمًا ولا يمكن تعطيلها بشكل عام.',
|
||||
'admin.notifications.adminWebhookPanel.title': 'Webhook المسؤول',
|
||||
'admin.notifications.adminWebhookPanel.hint':
|
||||
'يُستخدم هذا الـ Webhook حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن Webhooks المستخدمين ويُرسل تلقائيًا عند تعيين رابط URL.',
|
||||
'admin.notifications.adminWebhookPanel.saved': 'تم حفظ رابط Webhook المسؤول',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess':
|
||||
'تم إرسال Webhook الاختباري بنجاح',
|
||||
'admin.notifications.adminWebhookPanel.testFailed':
|
||||
'فشل إرسال Webhook الاختباري',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint':
|
||||
'يُرسل Webhook المسؤول تلقائيًا عند تعيين رابط URL',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess': 'تم إرسال Webhook الاختباري بنجاح',
|
||||
'admin.notifications.adminWebhookPanel.testFailed': 'فشل إرسال Webhook الاختباري',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint': 'يُرسل Webhook المسؤول تلقائيًا عند تعيين رابط URL',
|
||||
'admin.notifications.adminNtfyPanel.title': 'Ntfy المسؤول',
|
||||
'admin.notifications.adminNtfyPanel.hint':
|
||||
'يُستخدم موضوع Ntfy هذا حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن مواضيع المستخدمين ويُرسل دائمًا عند تهيئته.',
|
||||
@@ -39,23 +34,19 @@ const admin: TranslationStrings = {
|
||||
'admin.notifications.adminNtfyPanel.tokenCleared': 'تم مسح رمز وصول المسؤول',
|
||||
'admin.notifications.adminNtfyPanel.saved': 'تم حفظ إعدادات Ntfy للمسؤول',
|
||||
'admin.notifications.adminNtfyPanel.test': 'إرسال Ntfy تجريبي',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess':
|
||||
'تم إرسال Ntfy التجريبي بنجاح',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess': 'تم إرسال Ntfy التجريبي بنجاح',
|
||||
'admin.notifications.adminNtfyPanel.testFailed': 'فشل إرسال Ntfy التجريبي',
|
||||
'admin.notifications.adminNtfyPanel.alwaysOnHint':
|
||||
'يُرسل Ntfy للمسؤول دائمًا عند تهيئة موضوع',
|
||||
'admin.notifications.adminNtfyPanel.alwaysOnHint': 'يُرسل Ntfy للمسؤول دائمًا عند تهيئة موضوع',
|
||||
'admin.notifications.adminNotificationsHint':
|
||||
'حدد القنوات التي تُسلّم إشعارات المسؤول (مثل تنبيهات الإصدارات). يُرسل الـ Webhook تلقائيًا عند تعيين رابط URL لـ Webhook المسؤول.',
|
||||
'admin.notifications.tripReminders.title': 'تذكيرات الرحلات',
|
||||
'admin.notifications.tripReminders.hint':
|
||||
'إرسال تذكير قبل بدء الرحلة (يتطلب تعيين أيام التذكير على الرحلة).',
|
||||
'admin.notifications.tripReminders.hint': 'إرسال تذكير قبل بدء الرحلة (يتطلب تعيين أيام التذكير على الرحلة).',
|
||||
'admin.notifications.tripReminders.enabled': 'تم تفعيل تذكيرات الرحلات',
|
||||
'admin.notifications.tripReminders.disabled': 'تم تعطيل تذكيرات الرحلات',
|
||||
'admin.smtp.title': 'البريد والإشعارات',
|
||||
'admin.smtp.hint': 'تكوين SMTP لإرسال إشعارات البريد الإلكتروني.',
|
||||
'admin.smtp.testButton': 'إرسال بريد تجريبي',
|
||||
'admin.webhook.hint':
|
||||
'إرسال الإشعارات إلى webhook خارجي (Discord، Slack، إلخ).',
|
||||
'admin.webhook.hint': 'إرسال الإشعارات إلى webhook خارجي (Discord، Slack، إلخ).',
|
||||
'admin.smtp.testSuccess': 'تم إرسال البريد التجريبي بنجاح',
|
||||
'admin.smtp.testFailed': 'فشل إرسال البريد التجريبي',
|
||||
'admin.title': 'الإدارة',
|
||||
@@ -91,8 +82,7 @@ const admin: TranslationStrings = {
|
||||
'admin.toast.cannotDeleteSelf': 'لا يمكنك حذف حسابك الخاص',
|
||||
'admin.toast.userCreated': 'تم إنشاء المستخدم',
|
||||
'admin.toast.createError': 'فشل إنشاء المستخدم',
|
||||
'admin.toast.fieldsRequired':
|
||||
'اسم المستخدم والبريد الإلكتروني وكلمة المرور مطلوبة',
|
||||
'admin.toast.fieldsRequired': 'اسم المستخدم والبريد الإلكتروني وكلمة المرور مطلوبة',
|
||||
'admin.createUser': 'إنشاء مستخدم',
|
||||
'admin.invite.title': 'روابط الدعوة',
|
||||
'admin.invite.subtitle': 'إنشاء روابط تسجيل للاستخدام المحدود',
|
||||
@@ -116,14 +106,11 @@ const admin: TranslationStrings = {
|
||||
'admin.allowRegistration': 'السماح بالتسجيل',
|
||||
'admin.allowRegistrationHint': 'يمكن للمستخدمين الجدد التسجيل بأنفسهم',
|
||||
'admin.requireMfa': 'فرض المصادقة الثنائية (2FA)',
|
||||
'admin.requireMfaHint':
|
||||
'يجب على المستخدمين الذين لا يملكون 2FA إكمال الإعداد في الإعدادات قبل استخدام التطبيق.',
|
||||
'admin.requireMfaHint': 'يجب على المستخدمين الذين لا يملكون 2FA إكمال الإعداد في الإعدادات قبل استخدام التطبيق.',
|
||||
'admin.apiKeys': 'مفاتيح API',
|
||||
'admin.apiKeysHint':
|
||||
'اختياري. يُفعّل بيانات الأماكن الموسعة مثل الصور والطقس.',
|
||||
'admin.apiKeysHint': 'اختياري. يُفعّل بيانات الأماكن الموسعة مثل الصور والطقس.',
|
||||
'admin.mapsKey': 'مفتاح Google Maps API',
|
||||
'admin.mapsKeyHint':
|
||||
'مطلوب للبحث عن الأماكن. احصل عليه من console.cloud.google.com',
|
||||
'admin.mapsKeyHint': 'مطلوب للبحث عن الأماكن. احصل عليه من console.cloud.google.com',
|
||||
'admin.mapsKeyHintLong':
|
||||
'بدون مفتاح API، يُستخدم OpenStreetMap للبحث. مع مفتاح Google يمكن تحميل الصور والتقييمات وساعات العمل أيضًا. احصل عليه من console.cloud.google.com.',
|
||||
'admin.recommended': 'مُوصى به',
|
||||
@@ -134,27 +121,22 @@ const admin: TranslationStrings = {
|
||||
'admin.keyInvalid': 'غير صالح',
|
||||
'admin.keySaved': 'تم حفظ مفاتيح API',
|
||||
'admin.oidcTitle': 'تسجيل الدخول الموحد (OIDC)',
|
||||
'admin.oidcSubtitle':
|
||||
'السماح بتسجيل الدخول عبر مزودين خارجيين مثل Google أو Apple أو Authentik أو Keycloak.',
|
||||
'admin.oidcSubtitle': 'السماح بتسجيل الدخول عبر مزودين خارجيين مثل Google أو Apple أو Authentik أو Keycloak.',
|
||||
'admin.oidcDisplayName': 'الاسم المعروض',
|
||||
'admin.oidcIssuer': 'عنوان URL للمُصدر',
|
||||
'admin.oidcIssuerHint':
|
||||
'عنوان OpenID Connect Issuer URL للمزود. مثال: https://accounts.google.com',
|
||||
'admin.oidcIssuerHint': 'عنوان OpenID Connect Issuer URL للمزود. مثال: https://accounts.google.com',
|
||||
'admin.oidcSaved': 'تم حفظ إعدادات OIDC',
|
||||
'admin.oidcOnlyMode': 'تعطيل المصادقة بكلمة المرور',
|
||||
'admin.oidcOnlyModeHint':
|
||||
'عند التفعيل، يُسمح فقط بتسجيل الدخول عبر SSO. سيتم حظر تسجيل الدخول والتسجيل بكلمة المرور.',
|
||||
'admin.fileTypes': 'أنواع الملفات المسموح بها',
|
||||
'admin.fileTypesHint': 'حدد أنواع الملفات التي يمكن للمستخدمين رفعها.',
|
||||
'admin.fileTypesFormat':
|
||||
'امتدادات مفصولة بفواصل (مثل jpg,png,pdf,doc). استخدم * للسماح بجميع الأنواع.',
|
||||
'admin.fileTypesFormat': 'امتدادات مفصولة بفواصل (مثل jpg,png,pdf,doc). استخدم * للسماح بجميع الأنواع.',
|
||||
'admin.fileTypesSaved': 'تم حفظ إعدادات أنواع الملفات',
|
||||
'admin.placesPhotos.title': 'صور الأماكن',
|
||||
'admin.placesPhotos.subtitle':
|
||||
'جلب الصور من Google Places API. عطّلها للحفاظ على حصة API. صور Wikimedia غير متأثرة.',
|
||||
'admin.placesPhotos.subtitle': 'جلب الصور من Google Places API. عطّلها للحفاظ على حصة API. صور Wikimedia غير متأثرة.',
|
||||
'admin.placesAutocomplete.title': 'الإكمال التلقائي للأماكن',
|
||||
'admin.placesAutocomplete.subtitle':
|
||||
'استخدام Google Places API لاقتراحات البحث. عطّلها للحفاظ على حصة API.',
|
||||
'admin.placesAutocomplete.subtitle': 'استخدام Google Places API لاقتراحات البحث. عطّلها للحفاظ على حصة API.',
|
||||
'admin.placesDetails.title': 'تفاصيل الأماكن',
|
||||
'admin.placesDetails.subtitle':
|
||||
'جلب معلومات تفصيلية عن الأماكن (الساعات، التقييم، الموقع) من Google Places API. عطّلها للحفاظ على حصة API.',
|
||||
@@ -206,15 +188,12 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.vacay.name': 'الإجازة',
|
||||
'admin.addons.catalog.vacay.description': 'مخطط إجازات شخصي مع عرض تقويم',
|
||||
'admin.addons.catalog.atlas.name': 'الأطلس',
|
||||
'admin.addons.catalog.atlas.description':
|
||||
'خريطة العالم مع الدول التي تمت زيارتها وإحصائيات السفر',
|
||||
'admin.addons.catalog.atlas.description': 'خريطة العالم مع الدول التي تمت زيارتها وإحصائيات السفر',
|
||||
'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.description': 'شارك صور رحلتك عبر Immich',
|
||||
'admin.addons.catalog.mcp.description':
|
||||
'بروتوكول سياق النموذج لتكامل مساعد الذكاء الاصطناعي',
|
||||
'admin.addons.catalog.mcp.description': 'بروتوكول سياق النموذج لتكامل مساعد الذكاء الاصطناعي',
|
||||
'admin.addons.subtitleBefore': 'فعّل أو عطّل الميزات لتخصيص تجربة ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.enabled': 'مفعّل',
|
||||
@@ -224,8 +203,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.type.integration': 'تكامل',
|
||||
'admin.addons.tripHint': 'متاح كعلامة تبويب داخل كل رحلة',
|
||||
'admin.addons.globalHint': 'متاح كقسم مستقل في التنقل الرئيسي',
|
||||
'admin.addons.integrationHint':
|
||||
'خدمات الواجهة الخلفية وتكاملات API بدون صفحة مخصصة',
|
||||
'admin.addons.integrationHint': 'خدمات الواجهة الخلفية وتكاملات API بدون صفحة مخصصة',
|
||||
'admin.addons.toast.updated': 'تم تحديث الإضافة',
|
||||
'admin.addons.toast.error': 'فشل تحديث الإضافة',
|
||||
'admin.addons.noAddons': 'لا توجد إضافات متاحة',
|
||||
@@ -236,8 +214,7 @@ const admin: TranslationStrings = {
|
||||
'admin.weather.forecast': 'توقعات 16 يومًا',
|
||||
'admin.weather.forecastDesc': 'سابقًا 5 أيام (OpenWeatherMap)',
|
||||
'admin.weather.climate': 'بيانات المناخ التاريخية',
|
||||
'admin.weather.climateDesc':
|
||||
'متوسطات آخر 85 سنة للأيام بعد توقعات الـ 16 يومًا',
|
||||
'admin.weather.climateDesc': 'متوسطات آخر 85 سنة للأيام بعد توقعات الـ 16 يومًا',
|
||||
'admin.weather.requests': '10,000 طلب / يوم',
|
||||
'admin.weather.requestsDesc': 'مجاني، بدون مفتاح API',
|
||||
'admin.weather.locationHint':
|
||||
@@ -253,8 +230,7 @@ const admin: TranslationStrings = {
|
||||
'admin.mcpTokens.never': 'أبداً',
|
||||
'admin.mcpTokens.empty': 'لم يتم إنشاء أي رموز MCP بعد',
|
||||
'admin.mcpTokens.deleteTitle': 'حذف الرمز',
|
||||
'admin.mcpTokens.deleteMessage':
|
||||
'سيتم إلغاء هذا الرمز فوراً. سيفقد المستخدم وصوله إلى MCP عبر هذا الرمز.',
|
||||
'admin.mcpTokens.deleteMessage': 'سيتم إلغاء هذا الرمز فوراً. سيفقد المستخدم وصوله إلى MCP عبر هذا الرمز.',
|
||||
'admin.mcpTokens.deleteSuccess': 'تم حذف الرمز',
|
||||
'admin.mcpTokens.deleteError': 'فشل حذف الرمز',
|
||||
'admin.mcpTokens.loadError': 'فشل تحميل الرموز',
|
||||
@@ -265,13 +241,11 @@ const admin: TranslationStrings = {
|
||||
'admin.oauthSessions.created': 'تاريخ الإنشاء',
|
||||
'admin.oauthSessions.empty': 'لا توجد جلسات OAuth نشطة',
|
||||
'admin.oauthSessions.revokeTitle': 'إلغاء الجلسة',
|
||||
'admin.oauthSessions.revokeMessage':
|
||||
'سيتم إلغاء جلسة OAuth هذه فوراً. سيفقد العميل وصوله إلى MCP.',
|
||||
'admin.oauthSessions.revokeMessage': 'سيتم إلغاء جلسة OAuth هذه فوراً. سيفقد العميل وصوله إلى MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'تم إلغاء الجلسة',
|
||||
'admin.oauthSessions.revokeError': 'فشل إلغاء الجلسة',
|
||||
'admin.oauthSessions.loadError': 'فشل تحميل جلسات OAuth',
|
||||
'admin.audit.subtitle':
|
||||
'أحداث الأمان والإدارة (النسخ الاحتياطية، المستخدمون، المصادقة الثنائية، الإعدادات).',
|
||||
'admin.audit.subtitle': 'أحداث الأمان والإدارة (النسخ الاحتياطية، المستخدمون، المصادقة الثنائية، الإعدادات).',
|
||||
'admin.audit.empty': 'لا توجد سجلات تدقيق بعد.',
|
||||
'admin.audit.refresh': 'تحديث',
|
||||
'admin.audit.loadMore': 'تحميل المزيد',
|
||||
@@ -298,12 +272,10 @@ const admin: TranslationStrings = {
|
||||
'admin.update.button': 'عرض على GitHub',
|
||||
'admin.update.install': 'تثبيت التحديث',
|
||||
'admin.update.confirmTitle': 'تثبيت التحديث؟',
|
||||
'admin.update.confirmText':
|
||||
'سيتم تحديث TREK من {current} إلى {version}. سيُعاد تشغيل الخادم تلقائيًا بعد ذلك.',
|
||||
'admin.update.confirmText': 'سيتم تحديث TREK من {current} إلى {version}. سيُعاد تشغيل الخادم تلقائيًا بعد ذلك.',
|
||||
'admin.update.dataInfo':
|
||||
'جميع بياناتك (الرحلات، المستخدمون، مفاتيح API، المرفوعات، الإجازة، الأطلس، الميزانيات) ستبقى محفوظة.',
|
||||
'admin.update.warning':
|
||||
'سيكون التطبيق غير متاح لفترة وجيزة أثناء إعادة التشغيل.',
|
||||
'admin.update.warning': 'سيكون التطبيق غير متاح لفترة وجيزة أثناء إعادة التشغيل.',
|
||||
'admin.update.confirm': 'حدّث الآن',
|
||||
'admin.update.installing': 'جارٍ التحديث…',
|
||||
'admin.update.success': 'تم تثبيت التحديث. ستتم إعادة تشغيل الخادم…',
|
||||
@@ -311,8 +283,7 @@ const admin: TranslationStrings = {
|
||||
'admin.update.backupHint': 'نوصي بإنشاء نسخة احتياطية قبل التحديث.',
|
||||
'admin.update.backupLink': 'الذهاب إلى النسخ الاحتياطي',
|
||||
'admin.update.howTo': 'كيفية التحديث',
|
||||
'admin.update.dockerText':
|
||||
'يعمل TREK الخاص بك في Docker. للتحديث إلى {version}، نفّذ الأوامر التالية على الخادم:',
|
||||
'admin.update.dockerText': 'يعمل TREK الخاص بك في Docker. للتحديث إلى {version}، نفّذ الأوامر التالية على الخادم:',
|
||||
'admin.update.reloadHint': 'يرجى إعادة تحميل الصفحة بعد بضع ثوانٍ.',
|
||||
'admin.tabs.permissions': 'الصلاحيات',
|
||||
'admin.notifications.webhook': 'Webhook', // en-fallback
|
||||
@@ -326,13 +297,11 @@ const admin: TranslationStrings = {
|
||||
'admin.passwordLogin': 'Password Login', // en-fallback
|
||||
'admin.passwordLoginHint': 'Allow users to sign in with email and password', // en-fallback
|
||||
'admin.passwordRegistration': 'Password Registration', // en-fallback
|
||||
'admin.passwordRegistrationHint':
|
||||
'Allow new users to register with email and password', // en-fallback
|
||||
'admin.passwordRegistrationHint': 'Allow new users to register with email and password', // en-fallback
|
||||
'admin.oidcLogin': 'SSO Login', // en-fallback
|
||||
'admin.oidcLoginHint': 'Allow users to sign in with SSO', // en-fallback
|
||||
'admin.oidcRegistration': 'SSO Auto-Provisioning', // en-fallback
|
||||
'admin.oidcRegistrationHint':
|
||||
'Automatically create accounts for new SSO users', // en-fallback
|
||||
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users', // en-fallback
|
||||
'admin.envOverrideHint':
|
||||
'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
|
||||
@@ -342,8 +311,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Trip tracking & travel journal with check-ins, photos, and daily stories', // en-fallback
|
||||
'admin.passkey.title': 'تسجيل الدخول بمفتاح المرور',
|
||||
'admin.passkey.cardHint':
|
||||
'اسمح للمستخدمين بتسجيل الدخول باستخدام مفاتيح المرور (WebAuthn). معطّل افتراضيًا.',
|
||||
'admin.passkey.cardHint': 'اسمح للمستخدمين بتسجيل الدخول باستخدام مفاتيح المرور (WebAuthn). معطّل افتراضيًا.',
|
||||
'admin.passkey.login': 'تفعيل تسجيل الدخول بمفتاح المرور',
|
||||
'admin.passkey.loginHint':
|
||||
'إظهار خيار "تسجيل الدخول باستخدام مفتاح المرور" والسماح للمستخدمين بتسجيل مفاتيح المرور في إعداداتهم.',
|
||||
@@ -353,19 +321,20 @@ const admin: TranslationStrings = {
|
||||
'admin.passkey.rpIdHint':
|
||||
'النطاق المجرّد الذي تُربط به مفاتيح المرور، مثل trek.example.org. اتركه فارغًا لاشتقاقه من APP_URL. تغييره لاحقًا يُبطل مفاتيح المرور الموجودة.',
|
||||
'admin.passkey.origins': 'الأصول المسموح بها',
|
||||
'admin.passkey.originsHint':
|
||||
'أصول كاملة مفصولة بفواصل، مثل https://trek.example.org. اتركه فارغًا لاستخدام APP_URL.',
|
||||
'admin.passkey.originsHint': 'أصول كاملة مفصولة بفواصل، مثل https://trek.example.org. اتركه فارغًا لاستخدام APP_URL.',
|
||||
'admin.passkey.reset': 'إعادة تعيين مفاتيح المرور',
|
||||
'admin.passkey.resetHint':
|
||||
'إزالة جميع مفاتيح المرور لهذا المستخدم (مثلًا عند فقدان جهاز). سيظل بإمكانه تسجيل الدخول بكلمة المرور.',
|
||||
'admin.passkey.resetConfirm': 'إزالة جميع مفاتيح المرور لـ {name}؟',
|
||||
'admin.passkey.resetDone': 'تمت إزالة {count} من مفاتيح المرور',
|
||||
'admin.defaultSettings.mapProvider': 'محرك الخرائط',
|
||||
'admin.defaultSettings.mapProviderHint': 'الخريطة الافتراضية لجميع المستخدمين على هذا الخادم. لا يزال بإمكان كل مستخدم تجاوزها في إعداداته الخاصة.',
|
||||
'admin.defaultSettings.mapProviderHint':
|
||||
'الخريطة الافتراضية لجميع المستخدمين على هذا الخادم. لا يزال بإمكان كل مستخدم تجاوزها في إعداداته الخاصة.',
|
||||
'admin.defaultSettings.providerLeaflet': 'قياسي (مجاني)',
|
||||
'admin.defaultSettings.providerMapbox': 'Mapbox (ثلاثي الأبعاد)',
|
||||
'admin.defaultSettings.mapboxToken': 'رمز Mapbox المشترك',
|
||||
'admin.defaultSettings.mapboxTokenHint': 'يُستخدم لكل مستخدم لم يُدخل رمزه الخاص — حتى يحصل الخادم بأكمله على Mapbox دون مشاركة المفتاح بشكل فردي. يُخزَّن مشفّرًا.',
|
||||
'admin.defaultSettings.mapboxTokenHint':
|
||||
'يُستخدم لكل مستخدم لم يُدخل رمزه الخاص — حتى يحصل الخادم بأكمله على Mapbox دون مشاركة المفتاح بشكل فردي. يُخزَّن مشفّرًا.',
|
||||
'admin.defaultSettings.mapboxStyle': 'نمط الخريطة',
|
||||
'admin.defaultSettings.mapboxStylePlaceholder': 'اختر نمطًا…',
|
||||
'admin.defaultSettings.mapbox3d': 'المباني والتضاريس ثلاثية الأبعاد',
|
||||
|
||||
@@ -12,10 +12,8 @@ const backup: TranslationStrings = {
|
||||
'backup.createFirst': 'إنشاء أول نسخة',
|
||||
'backup.download': 'تنزيل',
|
||||
'backup.restore': 'استعادة',
|
||||
'backup.confirm.restore':
|
||||
'استعادة النسخة "{name}"؟\n\nسيتم استبدال جميع البيانات الحالية بالنسخة.',
|
||||
'backup.confirm.uploadRestore':
|
||||
'رفع واستعادة النسخة "{name}"؟\n\nسيتم الكتابة فوق جميع البيانات الحالية.',
|
||||
'backup.confirm.restore': 'استعادة النسخة "{name}"؟\n\nسيتم استبدال جميع البيانات الحالية بالنسخة.',
|
||||
'backup.confirm.uploadRestore': 'رفع واستعادة النسخة "{name}"؟\n\nسيتم الكتابة فوق جميع البيانات الحالية.',
|
||||
'backup.confirm.delete': 'حذف النسخة "{name}"؟',
|
||||
'backup.toast.loadError': 'فشل تحميل النسخ الاحتياطية',
|
||||
'backup.toast.created': 'تم إنشاء النسخة الاحتياطية بنجاح',
|
||||
@@ -31,8 +29,7 @@ const backup: TranslationStrings = {
|
||||
'backup.auto.title': 'النسخ الاحتياطي التلقائي',
|
||||
'backup.auto.subtitle': 'نسخ احتياطي تلقائي وفق جدول زمني',
|
||||
'backup.auto.enable': 'تفعيل النسخ التلقائي',
|
||||
'backup.auto.enableHint':
|
||||
'سيتم إنشاء نسخ احتياطية تلقائيًا وفق الجدول المختار',
|
||||
'backup.auto.enableHint': 'سيتم إنشاء نسخ احتياطية تلقائيًا وفق الجدول المختار',
|
||||
'backup.auto.interval': 'الفترة',
|
||||
'backup.auto.hour': 'التنفيذ في الساعة',
|
||||
'backup.auto.hourHint': 'التوقيت المحلي للخادم (تنسيق {format})',
|
||||
@@ -68,8 +65,7 @@ const backup: TranslationStrings = {
|
||||
'backup.restoreConfirmTitle': 'استعادة النسخة الاحتياطية؟',
|
||||
'backup.restoreWarning':
|
||||
'سيتم استبدال جميع البيانات الحالية (الرحلات، الأماكن، المستخدمون، المرفوعات) بالنسخة نهائيًا. لا يمكن التراجع عن ذلك.',
|
||||
'backup.restoreTip':
|
||||
'نصيحة: أنشئ نسخة احتياطية للحالة الحالية قبل الاستعادة.',
|
||||
'backup.restoreTip': 'نصيحة: أنشئ نسخة احتياطية للحالة الحالية قبل الاستعادة.',
|
||||
'backup.restoreConfirm': 'نعم، استعادة',
|
||||
'backup.auto.envLocked': 'Docker', // en-fallback
|
||||
};
|
||||
|
||||
@@ -26,8 +26,7 @@ const budget: TranslationStrings = {
|
||||
'budget.byCategory': 'حسب الفئة',
|
||||
'budget.editTooltip': 'انقر للتعديل',
|
||||
'budget.linkedToReservation': 'مرتبط بحجز — عدّل الاسم هناك',
|
||||
'budget.confirm.deleteCategory':
|
||||
'هل تريد حذف الفئة "{name}" مع {count} إدخالات؟',
|
||||
'budget.confirm.deleteCategory': 'هل تريد حذف الفئة "{name}" مع {count} إدخالات؟',
|
||||
'budget.deleteCategory': 'حذف الفئة',
|
||||
'budget.perPerson': 'لكل شخص',
|
||||
'budget.paid': 'مدفوع',
|
||||
@@ -38,78 +37,85 @@ const budget: TranslationStrings = {
|
||||
'انقر على صورة العضو في بند الميزانية لتحديده باللون الأخضر — وهذا يعني أنه دفع. ثم تُظهر التسوية من يدين لمن وبكم.',
|
||||
'budget.netBalances': 'الأرصدة الصافية',
|
||||
'budget.categoriesLabel': 'فئات',
|
||||
"costs.you": "أنت",
|
||||
"costs.youShort": "أنت",
|
||||
"costs.youLower": "أنت",
|
||||
"costs.youOwe": "عليك",
|
||||
"costs.youOweSub": "عليك أن تدفع للآخرين",
|
||||
"costs.youreOwed": "لك",
|
||||
"costs.youreOwedSub": "على الآخرين أن يدفعوا لك",
|
||||
"costs.totalSpend": "إجمالي إنفاق الرحلة",
|
||||
"costs.totalSpendSub": "عبر جميع المسافرين",
|
||||
"costs.to": "إلى",
|
||||
"costs.from": "من",
|
||||
"costs.allSettled": "لقد سوّيت كل حساباتك",
|
||||
"costs.nothingOwed": "لا شيء مستحق لك",
|
||||
"costs.yourShare": "حصتك",
|
||||
"costs.youPaid": "أنت دفعت",
|
||||
"costs.expenses": "المصروفات",
|
||||
"costs.entries": "{count} إدخالات",
|
||||
"costs.searchPlaceholder": "ابحث في المصروفات…",
|
||||
"costs.filter.all": "الكل",
|
||||
"costs.filter.mine": "دفعتها أنا",
|
||||
"costs.filter.owed": "مستحق لي",
|
||||
"costs.addExpense": "إضافة مصروف",
|
||||
"costs.editExpense": "تعديل المصروف",
|
||||
"costs.noMatch": "لا توجد مصروفات تطابق بحثك.",
|
||||
"costs.emptyText": "لا توجد مصروفات بعد. أضف أول مصروف لك.",
|
||||
"costs.spent": "تم إنفاق {amount}",
|
||||
"costs.noDate": "بدون تاريخ",
|
||||
"costs.noOnePaid": "لم يدفع أحد بعد",
|
||||
"costs.youLent": "أقرضت {amount}",
|
||||
"costs.youBorrowed": "اقترضت {amount}",
|
||||
"costs.settleUp": "تسوية الحساب",
|
||||
"costs.history": "السجل",
|
||||
"costs.everyoneSquare": "الجميع متعادلون",
|
||||
"costs.nothingOutstanding": "لا توجد مدفوعات معلّقة الآن.",
|
||||
"costs.pay": "ادفع",
|
||||
"costs.pays": "يدفع",
|
||||
"costs.settle": "تسوية",
|
||||
"costs.balances": "الأرصدة",
|
||||
"costs.byCategory": "حسب الفئة",
|
||||
"costs.noCategories": "لا توجد مصروفات بعد.",
|
||||
"costs.settleHistory": "سجل التسويات",
|
||||
"costs.noSettlements": "لا توجد مدفوعات مسوّاة بعد.",
|
||||
"costs.paymentsSettled": "تمت تسوية {count} مدفوعات",
|
||||
"costs.paid": "مدفوع",
|
||||
"costs.undo": "تراجع",
|
||||
"costs.whatFor": "لأجل ماذا كان؟",
|
||||
"costs.namePlaceholder": "مثل: عشاء، هدايا تذكارية، وقود…",
|
||||
"costs.totalAmount": "المبلغ الإجمالي",
|
||||
"costs.currency": "العملة",
|
||||
"costs.day": "اليوم",
|
||||
"costs.rateLabel": "1 {from} بـ {to}",
|
||||
"costs.category": "الفئة",
|
||||
"costs.whoPaid": "من دفع؟",
|
||||
"costs.splitBetween": "تقسيم بالتساوي بين",
|
||||
"costs.pickSomeone": "اختر شخصًا واحدًا على الأقل للتقسيم معه.",
|
||||
"costs.splitSummary": "تقسيم على {count} · {amount} لكل واحد",
|
||||
"costs.cat.accommodation": "الإقامة",
|
||||
"costs.cat.food": "الطعام والشراب",
|
||||
"costs.cat.groceries": "البقالة",
|
||||
"costs.cat.transport": "النقل",
|
||||
"costs.cat.flights": "الرحلات الجوية",
|
||||
"costs.cat.activities": "الأنشطة",
|
||||
"costs.cat.sightseeing": "معالم سياحية",
|
||||
"costs.cat.shopping": "التسوق",
|
||||
"costs.cat.fees": "الرسوم والتذاكر",
|
||||
"costs.cat.health": "الصحة",
|
||||
"costs.cat.tips": "البقشيش",
|
||||
"costs.cat.other": "أخرى",
|
||||
"costs.daysCount": "{count} أيام",
|
||||
"costs.travelers": "{count} مسافرين",
|
||||
"costs.liveRate": "سعر مباشر",
|
||||
"costs.settleAll": "تسوية الكل",
|
||||
'costs.you': 'أنت',
|
||||
'costs.youShort': 'أنت',
|
||||
'costs.youLower': 'أنت',
|
||||
'costs.youOwe': 'عليك',
|
||||
'costs.youOweSub': 'عليك أن تدفع للآخرين',
|
||||
'costs.youreOwed': 'لك',
|
||||
'costs.youreOwedSub': 'على الآخرين أن يدفعوا لك',
|
||||
'costs.totalSpend': 'إجمالي إنفاق الرحلة',
|
||||
'costs.totalSpendSub': 'عبر جميع المسافرين',
|
||||
'costs.to': 'إلى',
|
||||
'costs.from': 'من',
|
||||
'costs.allSettled': 'لقد سوّيت كل حساباتك',
|
||||
'costs.nothingOwed': 'لا شيء مستحق لك',
|
||||
'costs.yourShare': 'حصتك',
|
||||
'costs.youPaid': 'أنت دفعت',
|
||||
'costs.expenses': 'المصروفات',
|
||||
'costs.entries': '{count} إدخالات',
|
||||
'costs.searchPlaceholder': 'ابحث في المصروفات…',
|
||||
'costs.filter.all': 'الكل',
|
||||
'costs.filter.mine': 'دفعتها أنا',
|
||||
'costs.filter.owed': 'مستحق لي',
|
||||
'costs.addExpense': 'إضافة مصروف',
|
||||
'costs.editExpense': 'تعديل المصروف',
|
||||
'costs.noMatch': 'لا توجد مصروفات تطابق بحثك.',
|
||||
'costs.emptyText': 'لا توجد مصروفات بعد. أضف أول مصروف لك.',
|
||||
'costs.spent': 'تم إنفاق {amount}',
|
||||
'costs.noDate': 'بدون تاريخ',
|
||||
'costs.noOnePaid': 'لم يدفع أحد بعد',
|
||||
'costs.youLent': 'أقرضت {amount}',
|
||||
'costs.youBorrowed': 'اقترضت {amount}',
|
||||
'costs.settleUp': 'تسوية الحساب',
|
||||
'costs.history': 'السجل',
|
||||
'costs.everyoneSquare': 'الجميع متعادلون',
|
||||
'costs.nothingOutstanding': 'لا توجد مدفوعات معلّقة الآن.',
|
||||
'costs.pay': 'ادفع',
|
||||
'costs.pays': 'يدفع',
|
||||
'costs.settle': 'تسوية',
|
||||
'costs.balances': 'الأرصدة',
|
||||
'costs.byCategory': 'حسب الفئة',
|
||||
'costs.noCategories': 'لا توجد مصروفات بعد.',
|
||||
'costs.settleHistory': 'سجل التسويات',
|
||||
'costs.noSettlements': 'لا توجد مدفوعات مسوّاة بعد.',
|
||||
'costs.paymentsSettled': 'تمت تسوية {count} مدفوعات',
|
||||
'costs.paid': 'مدفوع',
|
||||
'costs.undo': 'تراجع',
|
||||
'costs.whatFor': 'لأجل ماذا كان؟',
|
||||
'costs.namePlaceholder': 'مثل: عشاء، هدايا تذكارية، وقود…',
|
||||
'costs.totalAmount': 'المبلغ الإجمالي',
|
||||
'costs.currency': 'العملة',
|
||||
'costs.day': 'اليوم',
|
||||
'costs.rateLabel': '1 {from} بـ {to}',
|
||||
'costs.category': 'الفئة',
|
||||
'costs.whoPaid': 'من دفع؟',
|
||||
'costs.splitBetween': 'تقسيم بالتساوي بين',
|
||||
'costs.pickSomeone': 'اختر شخصًا واحدًا على الأقل للتقسيم معه.',
|
||||
'costs.splitSummary': 'تقسيم على {count} · {amount} لكل واحد',
|
||||
'costs.cat.accommodation': 'الإقامة',
|
||||
'costs.cat.food': 'الطعام والشراب',
|
||||
'costs.cat.groceries': 'البقالة',
|
||||
'costs.cat.transport': 'النقل',
|
||||
'costs.cat.flights': 'الرحلات الجوية',
|
||||
'costs.cat.activities': 'الأنشطة',
|
||||
'costs.cat.sightseeing': 'معالم سياحية',
|
||||
'costs.cat.shopping': 'التسوق',
|
||||
'costs.cat.fees': 'الرسوم والتذاكر',
|
||||
'costs.cat.health': 'الصحة',
|
||||
'costs.cat.tips': 'البقشيش',
|
||||
'costs.cat.other': 'أخرى',
|
||||
'costs.daysCount': '{count} أيام',
|
||||
'costs.travelers': '{count} مسافرين',
|
||||
'costs.liveRate': 'سعر مباشر',
|
||||
'costs.settleAll': 'تسوية الكل',
|
||||
'costs.payment': 'دفعة',
|
||||
'costs.editPayment': 'تعديل الدفعة',
|
||||
'costs.addPayment': 'إضافة دفعة',
|
||||
'costs.unfinished': 'غير مكتمل',
|
||||
'costs.unfinishedHint': 'في الإجمالي فقط — لم تتم التسوية بعد',
|
||||
'costs.tapToInclude': 'اضغط للتضمين',
|
||||
'costs.amount': 'المبلغ',
|
||||
};
|
||||
|
||||
export default budget;
|
||||
|
||||
@@ -13,8 +13,7 @@ const categories: TranslationStrings = {
|
||||
'categories.defaultName': 'فئة',
|
||||
'categories.update': 'تحديث',
|
||||
'categories.create': 'إنشاء',
|
||||
'categories.confirm.delete':
|
||||
'حذف الفئة؟ لن يتم حذف الأماكن التابعة لهذه الفئة.',
|
||||
'categories.confirm.delete': 'حذف الفئة؟ لن يتم حذف الأماكن التابعة لهذه الفئة.',
|
||||
'categories.toast.loadError': 'فشل تحميل الفئات',
|
||||
'categories.toast.nameRequired': 'يرجى إدخال اسم',
|
||||
'categories.toast.updated': 'تم تحديث الفئة',
|
||||
|
||||
@@ -20,8 +20,7 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.timezoneCustomTzPlaceholder': 'مثال: Asia/Riyadh',
|
||||
'dashboard.timezoneCustomAdd': 'إضافة',
|
||||
'dashboard.timezoneCustomErrorEmpty': 'أدخل معرّف منطقة زمنية',
|
||||
'dashboard.timezoneCustomErrorInvalid':
|
||||
'منطقة زمنية غير صالحة. استخدم صيغة مثل Asia/Riyadh',
|
||||
'dashboard.timezoneCustomErrorInvalid': 'منطقة زمنية غير صالحة. استخدم صيغة مثل Asia/Riyadh',
|
||||
'dashboard.timezoneCustomErrorDuplicate': 'مضافة بالفعل',
|
||||
'dashboard.emptyTitle': 'لا توجد رحلات بعد',
|
||||
'dashboard.emptyText': 'أنشئ رحلتك الأولى وابدأ التخطيط',
|
||||
@@ -55,8 +54,7 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.toast.restoreError': 'فشل الاستعادة',
|
||||
'dashboard.toast.copied': 'تم نسخ الرحلة!',
|
||||
'dashboard.toast.copyError': 'فشل نسخ الرحلة',
|
||||
'dashboard.confirm.delete':
|
||||
'حذف الرحلة "{title}"؟ سيتم حذف جميع الأماكن والخطط نهائيًا.',
|
||||
'dashboard.confirm.delete': 'حذف الرحلة "{title}"؟ سيتم حذف جميع الأماكن والخطط نهائيًا.',
|
||||
'dashboard.editTrip': 'تعديل الرحلة',
|
||||
'dashboard.createTrip': 'إنشاء رحلة جديدة',
|
||||
'dashboard.tripTitle': 'العنوان',
|
||||
@@ -66,10 +64,8 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.startDate': 'تاريخ البداية',
|
||||
'dashboard.endDate': 'تاريخ النهاية',
|
||||
'dashboard.dayCount': 'عدد الأيام',
|
||||
'dashboard.dayCountHint':
|
||||
'عدد الأيام المراد التخطيط لها عندما لا يتم تحديد تواريخ السفر.',
|
||||
'dashboard.noDateHint':
|
||||
'لا يوجد تاريخ محدد. سيتم إنشاء 7 أيام افتراضية ويمكنك تغيير ذلك لاحقًا.',
|
||||
'dashboard.dayCountHint': 'عدد الأيام المراد التخطيط لها عندما لا يتم تحديد تواريخ السفر.',
|
||||
'dashboard.noDateHint': 'لا يوجد تاريخ محدد. سيتم إنشاء 7 أيام افتراضية ويمكنك تغيير ذلك لاحقًا.',
|
||||
'dashboard.coverImage': 'صورة الغلاف',
|
||||
'dashboard.addCoverImage': 'إضافة صورة غلاف',
|
||||
'dashboard.addMembers': 'رفاق السفر',
|
||||
|
||||
@@ -7,8 +7,7 @@ const day: TranslationStrings = {
|
||||
'day.sunrise': 'شروق الشمس',
|
||||
'day.sunset': 'غروب الشمس',
|
||||
'day.hourlyForecast': 'التوقعات بالساعة',
|
||||
'day.climateHint':
|
||||
'متوسطات تاريخية — التوقعات الفعلية متاحة خلال 16 يومًا من هذا التاريخ.',
|
||||
'day.climateHint': 'متوسطات تاريخية — التوقعات الفعلية متاحة خلال 16 يومًا من هذا التاريخ.',
|
||||
'day.noWeather': 'لا تتوفر بيانات طقس. أضف مكانًا بإحداثيات.',
|
||||
'day.overview': 'ملخص اليوم',
|
||||
'day.accommodation': 'الإقامة',
|
||||
|
||||
@@ -3,18 +3,14 @@ import type { TranslationStrings } from '../types';
|
||||
const dayplan: TranslationStrings = {
|
||||
'dayplan.icsTooltip': 'تصدير التقويم (ICS)',
|
||||
'dayplan.emptyDay': 'لا توجد أماكن مخططة لهذا اليوم',
|
||||
'dayplan.cannotReorderTransport':
|
||||
'لا يمكن إعادة ترتيب الحجوزات ذات الوقت الثابت',
|
||||
'dayplan.cannotReorderTransport': 'لا يمكن إعادة ترتيب الحجوزات ذات الوقت الثابت',
|
||||
'dayplan.confirmRemoveTimeTitle': 'إزالة الوقت؟',
|
||||
'dayplan.confirmRemoveTimeBody':
|
||||
'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.',
|
||||
'dayplan.confirmRemoveTimeBody': 'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.',
|
||||
'dayplan.confirmRemoveTimeAction': 'إزالة الوقت ونقل',
|
||||
'dayplan.confirmDeleteNoteTitle': 'حذف الملاحظة؟',
|
||||
'dayplan.confirmDeleteNoteBody': 'سيتم حذف هذه الملاحظة نهائيًا.',
|
||||
'dayplan.cannotDropOnTimed':
|
||||
'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت',
|
||||
'dayplan.cannotBreakChronology':
|
||||
'سيؤدي هذا إلى كسر الترتيب الزمني للعناصر والحجوزات المجدولة',
|
||||
'dayplan.cannotDropOnTimed': 'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت',
|
||||
'dayplan.cannotBreakChronology': 'سيؤدي هذا إلى كسر الترتيب الزمني للعناصر والحجوزات المجدولة',
|
||||
'dayplan.addNote': 'إضافة ملاحظة',
|
||||
'dayplan.editNote': 'تعديل الملاحظة',
|
||||
'dayplan.noteAdd': 'إضافة ملاحظة',
|
||||
|
||||
@@ -55,8 +55,7 @@ const ar: NotificationLocale = {
|
||||
body: 'تلقينا طلبًا لإعادة تعيين كلمة المرور لحسابك في TREK. انقر على الزر أدناه لتعيين كلمة مرور جديدة.',
|
||||
ctaIntro: 'إعادة تعيين كلمة المرور',
|
||||
expiry: 'تنتهي صلاحية هذا الرابط خلال 60 دقيقة.',
|
||||
ignore:
|
||||
'إذا لم تطلب هذا، يمكنك تجاهل هذه الرسالة — لن تتغير كلمة المرور الخاصة بك.',
|
||||
ignore: 'إذا لم تطلب هذا، يمكنك تجاهل هذه الرسالة — لن تتغير كلمة المرور الخاصة بك.',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ const files: TranslationStrings = {
|
||||
'files.uploadError': 'فشل الرفع',
|
||||
'files.dropzone': 'أسقط الملفات هنا',
|
||||
'files.dropzoneHint': 'أو انقر للتصفح',
|
||||
'files.allowedTypes':
|
||||
'صور، PDF، DOC، DOCX، XLS، XLSX، TXT، CSV · حد أقصى 50 ميغابايت',
|
||||
'files.allowedTypes': 'صور، PDF، DOC، DOCX، XLS، XLSX، TXT، CSV · حد أقصى 50 ميغابايت',
|
||||
'files.uploading': 'جارٍ الرفع...',
|
||||
'files.filterAll': 'الكل',
|
||||
'files.filterPdf': 'ملفات PDF',
|
||||
@@ -52,10 +51,8 @@ const files: TranslationStrings = {
|
||||
'files.toast.assigned': 'تم إسناد الملف',
|
||||
'files.toast.assignError': 'فشل الإسناد',
|
||||
'files.toast.restoreError': 'فشلت الاستعادة',
|
||||
'files.confirm.permanentDelete':
|
||||
'حذف هذا الملف نهائيًا؟ لا يمكن التراجع عن ذلك.',
|
||||
'files.confirm.emptyTrash':
|
||||
'حذف جميع ملفات سلة المهملات نهائيًا؟ لا يمكن التراجع عن ذلك.',
|
||||
'files.confirm.permanentDelete': 'حذف هذا الملف نهائيًا؟ لا يمكن التراجع عن ذلك.',
|
||||
'files.confirm.emptyTrash': 'حذف جميع ملفات سلة المهملات نهائيًا؟ لا يمكن التراجع عن ذلك.',
|
||||
'files.noteLabel': 'ملاحظة',
|
||||
'files.notePlaceholder': 'أضف ملاحظة...',
|
||||
};
|
||||
|
||||
@@ -10,14 +10,12 @@ const journey: TranslationStrings = {
|
||||
'journey.detail.places': 'أماكن',
|
||||
'journey.skeletons.show': 'إظهار الاقتراحات',
|
||||
'journey.skeletons.hide': 'إخفاء الاقتراحات',
|
||||
'journey.editor.discardChangesConfirm':
|
||||
'لديك تغييرات غير محفوظة. هل تريد تجاهلها؟',
|
||||
'journey.editor.discardChangesConfirm': 'لديك تغييرات غير محفوظة. هل تريد تجاهلها؟',
|
||||
'journey.editor.uploadFailed': 'فشل رفع الصور',
|
||||
'journey.editor.uploadPhotos': 'رفع صور',
|
||||
'journey.editor.uploading': '...جارٍ الرفع',
|
||||
'journey.editor.uploadingProgress': 'جارٍ الرفع {done}/{total}…',
|
||||
'journey.editor.uploadPartialFailed':
|
||||
'فشل رفع {failed} من {total} — احفظ مجدداً للمحاولة',
|
||||
'journey.editor.uploadPartialFailed': 'فشل رفع {failed} من {total} — احفظ مجدداً للمحاولة',
|
||||
'journey.editor.fromGallery': 'من المعرض',
|
||||
'journey.editor.addAnother': 'إضافة آخر',
|
||||
'journey.editor.makeFirst': 'جعله الأول',
|
||||
@@ -33,8 +31,7 @@ const journey: TranslationStrings = {
|
||||
'journey.settings.reopenJourney': 'استعادة الرحلة',
|
||||
'journey.settings.archived': 'تم أرشفة الرحلة',
|
||||
'journey.settings.reopened': 'تمت إعادة فتح الرحلة',
|
||||
'journey.settings.endDescription':
|
||||
'يخفي شارة البث المباشر. يمكنك إعادة الفتح في أي وقت.',
|
||||
'journey.settings.endDescription': 'يخفي شارة البث المباشر. يمكنك إعادة الفتح في أي وقت.',
|
||||
'journey.settings.failedToDelete': 'فشل في الحذف',
|
||||
'journey.entries.deleteTitle': 'حذف الإدخال',
|
||||
'journey.photosUploaded': 'تم رفع {count} صورة',
|
||||
@@ -68,8 +65,7 @@ const journey: TranslationStrings = {
|
||||
'journey.notFound': 'Journey not found', // en-fallback
|
||||
'journey.photos': 'Photos', // en-fallback
|
||||
'journey.timelineEmpty': 'No stops yet', // en-fallback
|
||||
'journey.timelineEmptyHint':
|
||||
'Add a check-in or write a journal entry to get started', // en-fallback
|
||||
'journey.timelineEmptyHint': 'Add a check-in or write a journal entry to get started', // en-fallback
|
||||
'journey.status.draft': 'Draft', // en-fallback
|
||||
'journey.status.active': 'Active', // 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.bodyPlaceholder': 'Tell the story of this day...', // en-fallback
|
||||
'journey.editor.placePlaceholder': 'Location (optional)', // en-fallback
|
||||
'journey.editor.tagsPlaceholder':
|
||||
'Tags: hidden gem, best meal, must revisit...', // en-fallback
|
||||
'journey.editor.tagsPlaceholder': 'Tags: hidden gem, best meal, must revisit...', // en-fallback
|
||||
'journey.visibility.private': 'Private', // en-fallback
|
||||
'journey.visibility.shared': 'Shared', // en-fallback
|
||||
'journey.visibility.public': 'Public', // en-fallback
|
||||
'journey.emptyState.title': 'Your story starts here', // en-fallback
|
||||
'journey.emptyState.subtitle':
|
||||
'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.emptyState.subtitle': '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.createJourney': 'Create Journey', // en-fallback
|
||||
'journey.frontpage.activeJourney': 'Active Journey', // en-fallback
|
||||
'journey.frontpage.allJourneys': 'All Journeys', // en-fallback
|
||||
'journey.frontpage.journeys': 'journeys', // en-fallback
|
||||
'journey.frontpage.createNew': 'Create a new Journey', // en-fallback
|
||||
'journey.frontpage.createNewSub':
|
||||
'Pick trips, write stories, share your adventures', // en-fallback
|
||||
'journey.frontpage.createNewSub': 'Pick trips, write stories, share your adventures', // en-fallback
|
||||
'journey.frontpage.live': 'Live', // en-fallback
|
||||
'journey.frontpage.synced': 'Synced', // en-fallback
|
||||
'journey.frontpage.continueWriting': 'Continue writing', // en-fallback
|
||||
'journey.frontpage.updated': 'Updated {time}', // en-fallback
|
||||
'journey.frontpage.suggestionLabel': 'Trip just ended', // en-fallback
|
||||
'journey.frontpage.suggestionText':
|
||||
'Turn <strong>{title}</strong> into a Journey', // en-fallback
|
||||
'journey.frontpage.suggestionText': 'Turn <strong>{title}</strong> into a Journey', // en-fallback
|
||||
'journey.frontpage.dismiss': 'Dismiss', // en-fallback
|
||||
'journey.frontpage.journeyName': 'Journey Name', // 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.editEntry': 'Edit Entry', // en-fallback
|
||||
'journey.detail.noEntries': 'No entries yet', // en-fallback
|
||||
'journey.detail.noEntriesHint':
|
||||
'Add a trip to get started with skeleton entries', // en-fallback
|
||||
'journey.detail.noEntriesHint': 'Add a trip to get started with skeleton entries', // en-fallback
|
||||
'journey.detail.noPhotos': 'No photos yet', // en-fallback
|
||||
'journey.detail.noPhotosHint':
|
||||
'Upload photos to entries or browse your Immich/Synology library', // en-fallback
|
||||
'journey.detail.noPhotosHint': 'Upload photos to entries or browse your Immich/Synology library', // en-fallback
|
||||
'journey.detail.journeyTab': 'Journey', // en-fallback
|
||||
'journey.detail.journeyStats': 'Journey Stats', // 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.delete': 'Delete', // en-fallback
|
||||
'journey.settings.deleteJourney': 'Delete Journey', // en-fallback
|
||||
'journey.settings.deleteMessage':
|
||||
'Delete "{title}"? All entries and photos will be lost.', // en-fallback
|
||||
'journey.settings.deleteMessage': 'Delete "{title}"? All entries and photos will be lost.', // en-fallback
|
||||
'journey.settings.saved': 'Settings saved', // en-fallback
|
||||
'journey.settings.saveFailed': 'Failed to save', // en-fallback
|
||||
'journey.settings.coverUpdated': 'Cover updated', // en-fallback
|
||||
'journey.settings.coverFailed': 'Upload failed', // en-fallback
|
||||
'journey.public.notFound': 'Not Found', // en-fallback
|
||||
'journey.public.notFoundMessage':
|
||||
"This journey doesn't exist or the link has expired.", // en-fallback
|
||||
'journey.public.notFoundMessage': "This journey doesn't exist or the link has expired.", // en-fallback
|
||||
'journey.public.readOnly': 'Read-only · Public Journey', // en-fallback
|
||||
'journey.public.tagline': 'Travel Resource & Exploration Kit', // en-fallback
|
||||
'journey.public.sharedVia': 'Shared via', // en-fallback
|
||||
|
||||
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const login: TranslationStrings = {
|
||||
'login.error': 'فشل تسجيل الدخول. يرجى التحقق من بياناتك.',
|
||||
'login.tagline': 'رحلاتك.\nخطتك.',
|
||||
'login.description':
|
||||
'خطط لرحلاتك بشكل تعاوني مع خرائط تفاعلية وميزانيات ومزامنة لحظية.',
|
||||
'login.description': 'خطط لرحلاتك بشكل تعاوني مع خرائط تفاعلية وميزانيات ومزامنة لحظية.',
|
||||
'login.features.maps': 'خرائط تفاعلية',
|
||||
'login.features.mapsDesc': 'Google Places ومسارات وتجميع',
|
||||
'login.features.realtime': 'مزامنة فورية',
|
||||
@@ -43,8 +42,7 @@ const login: TranslationStrings = {
|
||||
'login.oidc.invalidState': 'جلسة غير صالحة. حاول مرة أخرى.',
|
||||
'login.demoFailed': 'فشل الدخول إلى العرض التجريبي',
|
||||
'login.oidcSignIn': 'تسجيل الدخول عبر {name}',
|
||||
'login.oidcOnly':
|
||||
'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
|
||||
'login.oidcOnly': 'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
|
||||
'login.oidcLoggedOut': 'تم تسجيل خروجك. سجّل الدخول مجدداً عبر مزود SSO.',
|
||||
'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل',
|
||||
'login.mfaTitle': 'المصادقة الثنائية',
|
||||
@@ -75,18 +73,14 @@ const login: TranslationStrings = {
|
||||
'login.passwordsDontMatch': 'كلمتا المرور غير متطابقتين',
|
||||
'login.mfaCode': 'رمز 2FA',
|
||||
'login.resetPasswordTitle': 'ضبط كلمة مرور جديدة',
|
||||
'login.resetPasswordBody':
|
||||
'اختر كلمة مرور قوية لم تستخدمها هنا من قبل. 8 أحرف على الأقل.',
|
||||
'login.resetPasswordMfaBody':
|
||||
'أدخل رمز 2FA أو رمز النسخ الاحتياطي لإتمام إعادة التعيين.',
|
||||
'login.resetPasswordBody': 'اختر كلمة مرور قوية لم تستخدمها هنا من قبل. 8 أحرف على الأقل.',
|
||||
'login.resetPasswordMfaBody': 'أدخل رمز 2FA أو رمز النسخ الاحتياطي لإتمام إعادة التعيين.',
|
||||
'login.resetPasswordSubmit': 'إعادة تعيين كلمة المرور',
|
||||
'login.resetPasswordVerify': 'تحقق وأعد التعيين',
|
||||
'login.resetPasswordSuccessTitle': 'تم تحديث كلمة المرور',
|
||||
'login.resetPasswordSuccessBody':
|
||||
'يمكنك الآن تسجيل الدخول بكلمة المرور الجديدة.',
|
||||
'login.resetPasswordSuccessBody': 'يمكنك الآن تسجيل الدخول بكلمة المرور الجديدة.',
|
||||
'login.resetPasswordInvalidLink': 'رابط إعادة تعيين غير صالح',
|
||||
'login.resetPasswordInvalidLinkBody':
|
||||
'هذا الرابط مفقود أو تالف. اطلب رابطًا جديدًا للمتابعة.',
|
||||
'login.resetPasswordInvalidLinkBody': 'هذا الرابط مفقود أو تالف. اطلب رابطًا جديدًا للمتابعة.',
|
||||
'login.resetPasswordFailed': 'فشلت إعادة التعيين. ربما انتهت صلاحية الرابط.',
|
||||
'login.emailPlaceholder': 'your@email.com', // en-fallback
|
||||
'login.passkey.signIn': 'تسجيل الدخول باستخدام مفتاح المرور',
|
||||
|
||||
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const memories: TranslationStrings = {
|
||||
'memories.title': 'صور',
|
||||
'memories.notConnected': 'Immich غير متصل',
|
||||
'memories.notConnectedHint':
|
||||
'قم بتوصيل Immich في الإعدادات لعرض صور رحلتك هنا.',
|
||||
'memories.notConnectedHint': 'قم بتوصيل Immich في الإعدادات لعرض صور رحلتك هنا.',
|
||||
'memories.notConnectedMultipleHint':
|
||||
'قم بتوصيل أحد موفري الصور هؤلاء: {provider_names} في الإعدادات لتتمكن من إضافة صور إلى هذه الرحلة.',
|
||||
'memories.noDates': 'أضف تواريخ لرحلتك لتحميل الصور.',
|
||||
@@ -24,8 +23,7 @@ const memories: TranslationStrings = {
|
||||
'memories.providerOTP': 'رمز MFA (إذا كان مفعلاً)',
|
||||
'memories.skipSSLVerification': 'تخطي التحقق من شهادة SSL',
|
||||
'memories.immichAutoUpload': 'نسخ صور الرحلة إلى Immich عند الرفع',
|
||||
'memories.providerUrlHintSynology':
|
||||
'أدرج مسار تطبيق Photos في URL، مثل https://nas:5001/photo',
|
||||
'memories.providerUrlHintSynology': 'أدرج مسار تطبيق Photos في URL، مثل https://nas:5001/photo',
|
||||
'memories.testConnection': 'اختبار الاتصال',
|
||||
'memories.testShort': 'اختبار',
|
||||
'memories.testFirst': 'اختبر الاتصال أولاً',
|
||||
@@ -34,8 +32,7 @@ const memories: TranslationStrings = {
|
||||
'memories.connectionSuccess': 'تم الاتصال بـ Immich',
|
||||
'memories.connectionError': 'تعذر الاتصال بـ Immich',
|
||||
'memories.saved': 'تم حفظ إعدادات {provider_name}',
|
||||
'memories.providerDisconnectedBanner':
|
||||
'اتصالك بـ {provider_name} مفقود. أعد الاتصال في الإعدادات لعرض الصور.',
|
||||
'memories.providerDisconnectedBanner': 'اتصالك بـ {provider_name} مفقود. أعد الاتصال في الإعدادات لعرض الصور.',
|
||||
'memories.saveError': 'تعذّر حفظ إعدادات {provider_name}',
|
||||
'memories.addPhotos': 'إضافة صور',
|
||||
'memories.linkAlbum': 'ربط ألبوم',
|
||||
@@ -59,8 +56,7 @@ const memories: TranslationStrings = {
|
||||
'memories.tripDates': 'تواريخ الرحلة',
|
||||
'memories.allPhotos': 'جميع الصور',
|
||||
'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟',
|
||||
'memories.confirmShareHint':
|
||||
'{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
|
||||
'memories.confirmShareHint': '{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
|
||||
'memories.confirmShareButton': 'مشاركة الصور',
|
||||
'memories.error.loadAlbums': 'فشل تحميل الألبومات',
|
||||
'memories.error.linkAlbum': 'فشل ربط الألبوم',
|
||||
|
||||
@@ -35,7 +35,6 @@ const notif: TranslationStrings = {
|
||||
'notif.generic.title': 'إشعار',
|
||||
'notif.generic.text': 'لديك إشعار جديد',
|
||||
'notif.dev.unknown_event.title': '[DEV] حدث غير معروف',
|
||||
'notif.dev.unknown_event.text':
|
||||
'نوع الحدث "{event}" غير مسجل في EVENT_NOTIFICATION_CONFIG',
|
||||
'notif.dev.unknown_event.text': 'نوع الحدث "{event}" غير مسجل في EVENT_NOTIFICATION_CONFIG',
|
||||
};
|
||||
export default notif;
|
||||
|
||||
@@ -14,8 +14,7 @@ const notifications: TranslationStrings = {
|
||||
'notifications.delete': 'حذف',
|
||||
'notifications.system': 'النظام',
|
||||
'notifications.synologySessionCleared.title': 'تم قطع اتصال Synology Photos',
|
||||
'notifications.synologySessionCleared.text':
|
||||
'تغير خادمك أو حسابك — انتقل إلى الإعدادات لاختبار اتصالك مرة أخرى.',
|
||||
'notifications.synologySessionCleared.text': 'تغير خادمك أو حسابك — انتقل إلى الإعدادات لاختبار اتصالك مرة أخرى.',
|
||||
'notifications.versionAvailable.title': 'تحديث متاح',
|
||||
'notifications.versionAvailable.text': 'TREK {version} متاح الآن.',
|
||||
'notifications.versionAvailable.button': 'عرض التفاصيل',
|
||||
@@ -29,8 +28,7 @@ const notifications: TranslationStrings = {
|
||||
'notifications.test.navigateText': 'إشعار تجريبي للتنقل.',
|
||||
'notifications.test.goThere': 'اذهب إلى هناك',
|
||||
'notifications.test.adminTitle': 'إذاعة المسؤول',
|
||||
'notifications.test.adminText':
|
||||
'أرسل {actor} إشعاراً تجريبياً لجميع المسؤولين.',
|
||||
'notifications.test.adminText': 'أرسل {actor} إشعاراً تجريبياً لجميع المسؤولين.',
|
||||
'notifications.test.tripTitle': 'نشر {actor} في رحلتك',
|
||||
'notifications.test.tripText': 'إشعار تجريبي للرحلة "{trip}".',
|
||||
};
|
||||
|
||||
+28
-56
@@ -13,40 +13,29 @@ const oauth: TranslationStrings = {
|
||||
'oauth.scope.group.weather': 'الطقس',
|
||||
'oauth.scope.group.journey': 'مذكرة السفر',
|
||||
'oauth.scope.trips:read.label': 'عرض الرحلات وخطط السفر',
|
||||
'oauth.scope.trips:read.description':
|
||||
'قراءة الرحلات والأيام والملاحظات والأعضاء',
|
||||
'oauth.scope.trips:read.description': 'قراءة الرحلات والأيام والملاحظات والأعضاء',
|
||||
'oauth.scope.trips:write.label': 'تحرير الرحلات وخطط السفر',
|
||||
'oauth.scope.trips:write.description':
|
||||
'إنشاء وتحديث الرحلات والأيام والملاحظات وإدارة الأعضاء',
|
||||
'oauth.scope.trips:write.description': 'إنشاء وتحديث الرحلات والأيام والملاحظات وإدارة الأعضاء',
|
||||
'oauth.scope.trips:delete.label': 'حذف الرحلات',
|
||||
'oauth.scope.trips:delete.description':
|
||||
'حذف الرحلات بأكملها نهائياً — هذا الإجراء لا يمكن التراجع عنه',
|
||||
'oauth.scope.trips:delete.description': 'حذف الرحلات بأكملها نهائياً — هذا الإجراء لا يمكن التراجع عنه',
|
||||
'oauth.scope.trips:share.label': 'إدارة روابط المشاركة',
|
||||
'oauth.scope.trips:share.description':
|
||||
'إنشاء روابط مشاركة عامة وتحديثها وإلغاؤها',
|
||||
'oauth.scope.trips:share.description': 'إنشاء روابط مشاركة عامة وتحديثها وإلغاؤها',
|
||||
'oauth.scope.places:read.label': 'عرض الأماكن وبيانات الخريطة',
|
||||
'oauth.scope.places:read.description':
|
||||
'قراءة الأماكن وتعيينات الأيام والعلامات والفئات',
|
||||
'oauth.scope.places:read.description': 'قراءة الأماكن وتعيينات الأيام والعلامات والفئات',
|
||||
'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.description':
|
||||
'قراءة الدول والمناطق المزارة وقائمة الأمنيات',
|
||||
'oauth.scope.atlas:read.description': 'قراءة الدول والمناطق المزارة وقائمة الأمنيات',
|
||||
'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.description':
|
||||
'قراءة عناصر الأمتعة والحقائب ومُسنَدي الفئات',
|
||||
'oauth.scope.packing:read.description': 'قراءة عناصر الأمتعة والحقائب ومُسنَدي الفئات',
|
||||
'oauth.scope.packing:write.label': 'إدارة قوائم الأمتعة',
|
||||
'oauth.scope.packing:write.description':
|
||||
'إضافة وتحديث وحذف وتبديل وإعادة ترتيب عناصر الأمتعة والحقائب',
|
||||
'oauth.scope.packing:write.description': 'إضافة وتحديث وحذف وتبديل وإعادة ترتيب عناصر الأمتعة والحقائب',
|
||||
'oauth.scope.todos:read.label': 'عرض قوائم المهام',
|
||||
'oauth.scope.todos:read.description': 'قراءة مهام الرحلة ومُسنَدي الفئات',
|
||||
'oauth.scope.todos:write.label': 'إدارة قوائم المهام',
|
||||
'oauth.scope.todos:write.description':
|
||||
'إنشاء وتحديث وتبديل وحذف وإعادة ترتيب المهام',
|
||||
'oauth.scope.todos:write.description': 'إنشاء وتحديث وتبديل وحذف وإعادة ترتيب المهام',
|
||||
'oauth.scope.budget:read.label': 'عرض الميزانية',
|
||||
'oauth.scope.budget:read.description': 'قراءة بنود الميزانية وتفاصيل النفقات',
|
||||
'oauth.scope.budget:write.label': 'إدارة الميزانية',
|
||||
@@ -54,55 +43,40 @@ const oauth: TranslationStrings = {
|
||||
'oauth.scope.reservations:read.label': 'عرض الحجوزات',
|
||||
'oauth.scope.reservations:read.description': 'قراءة الحجوزات وتفاصيل الإقامة',
|
||||
'oauth.scope.reservations:write.label': 'إدارة الحجوزات',
|
||||
'oauth.scope.reservations:write.description':
|
||||
'إنشاء وتحديث وحذف وإعادة ترتيب الحجوزات',
|
||||
'oauth.scope.reservations:write.description': 'إنشاء وتحديث وحذف وإعادة ترتيب الحجوزات',
|
||||
'oauth.scope.collab:read.label': 'عرض التعاون',
|
||||
'oauth.scope.collab:read.description':
|
||||
'قراءة ملاحظات التعاون والاستطلاعات والرسائل',
|
||||
'oauth.scope.collab:read.description': 'قراءة ملاحظات التعاون والاستطلاعات والرسائل',
|
||||
'oauth.scope.collab:write.label': 'إدارة التعاون',
|
||||
'oauth.scope.collab:write.description':
|
||||
'إنشاء وتحديث وحذف الملاحظات والاستطلاعات والرسائل التعاونية',
|
||||
'oauth.scope.collab:write.description': 'إنشاء وتحديث وحذف الملاحظات والاستطلاعات والرسائل التعاونية',
|
||||
'oauth.scope.notifications:read.label': 'عرض الإشعارات',
|
||||
'oauth.scope.notifications:read.description':
|
||||
'قراءة إشعارات التطبيق وأعداد غير المقروءة',
|
||||
'oauth.scope.notifications:read.description': 'قراءة إشعارات التطبيق وأعداد غير المقروءة',
|
||||
'oauth.scope.notifications:write.label': 'إدارة الإشعارات',
|
||||
'oauth.scope.notifications:write.description':
|
||||
'تعليم الإشعارات كمقروءة والرد عليها',
|
||||
'oauth.scope.notifications:write.description': 'تعليم الإشعارات كمقروءة والرد عليها',
|
||||
'oauth.scope.vacay:read.label': 'عرض خطط الإجازة',
|
||||
'oauth.scope.vacay:read.description':
|
||||
'قراءة بيانات تخطيط الإجازة والإدخالات والإحصاءات',
|
||||
'oauth.scope.vacay:read.description': 'قراءة بيانات تخطيط الإجازة والإدخالات والإحصاءات',
|
||||
'oauth.scope.vacay:write.label': 'إدارة خطط الإجازة',
|
||||
'oauth.scope.vacay:write.description':
|
||||
'إنشاء وإدارة إدخالات الإجازة والعطلات وخطط الفريق',
|
||||
'oauth.scope.vacay:write.description': 'إنشاء وإدارة إدخالات الإجازة والعطلات وخطط الفريق',
|
||||
'oauth.scope.geo:read.label': 'الخرائط والترميز الجغرافي',
|
||||
'oauth.scope.geo:read.description':
|
||||
'البحث عن مواقع وحل عناوين الخرائط والترميز الجغرافي العكسي للإحداثيات',
|
||||
'oauth.scope.geo:read.description': 'البحث عن مواقع وحل عناوين الخرائط والترميز الجغرافي العكسي للإحداثيات',
|
||||
'oauth.scope.weather:read.label': 'توقعات الطقس',
|
||||
'oauth.scope.weather:read.description':
|
||||
'جلب توقعات الطقس لمواقع الرحلة وتواريخها',
|
||||
'oauth.scope.weather:read.description': 'جلب توقعات الطقس لمواقع الرحلة وتواريخها',
|
||||
'oauth.scope.journey:read.label': 'عرض مذكرات السفر',
|
||||
'oauth.scope.journey:read.description':
|
||||
'قراءة مذكرات السفر والمدخلات وقائمة المساهمين',
|
||||
'oauth.scope.journey:read.description': 'قراءة مذكرات السفر والمدخلات وقائمة المساهمين',
|
||||
'oauth.scope.journey:write.label': 'إدارة مذكرات السفر',
|
||||
'oauth.scope.journey:write.description':
|
||||
'إنشاء مذكرات السفر وتحديثها وحذفها وإدخالاتها',
|
||||
'oauth.scope.journey:write.description': 'إنشاء مذكرات السفر وتحديثها وحذفها وإدخالاتها',
|
||||
'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.geo': 'Geo', // en-fallback
|
||||
'oauth.authorize.authorizing': 'Authorizing…', // en-fallback
|
||||
'oauth.authorize.loading': 'Loading…', // en-fallback
|
||||
'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback
|
||||
'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback
|
||||
'oauth.authorize.loginDescription':
|
||||
'{client} wants access to your TREK account. Please sign in first.', // en-fallback
|
||||
'oauth.authorize.loginDescription': '{client} wants access to your TREK account. Please sign in first.', // en-fallback
|
||||
'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback
|
||||
'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback
|
||||
'oauth.authorize.requestDescription':
|
||||
'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.requestDescription': '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.selectScope': 'Select at least one scope', // en-fallback
|
||||
'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // 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.permissionsRequested': 'Permissions requested', // en-fallback
|
||||
'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback
|
||||
'oauth.authorize.alwaysTool.listTrips':
|
||||
'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.listTrips': '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
|
||||
};
|
||||
export default oauth;
|
||||
|
||||
@@ -7,8 +7,7 @@ const packing: TranslationStrings = {
|
||||
'packing.importTitle': 'استيراد قائمة التعبئة',
|
||||
'packing.importHint':
|
||||
'عنصر واحد لكل سطر. يمكن إضافة الفئة والكمية مفصولة بفاصلة أو فاصلة منقوطة أو علامة تبويب: الاسم، الفئة، الكمية',
|
||||
'packing.importPlaceholder':
|
||||
'فرشاة أسنان\nواقي شمس، نظافة\nقمصان، ملابس، 5\nجواز سفر، مستندات',
|
||||
'packing.importPlaceholder': 'فرشاة أسنان\nواقي شمس، نظافة\nقمصان، ملابس، 5\nجواز سفر، مستندات',
|
||||
'packing.importCsv': 'تحميل CSV/TXT',
|
||||
'packing.importAction': 'استيراد {count}',
|
||||
'packing.importSuccess': 'تم استيراد {count} عنصر',
|
||||
|
||||
@@ -32,25 +32,20 @@ const perm: TranslationStrings = {
|
||||
'perm.action.collab_edit': 'التعاون (ملاحظات، استطلاعات، دردشة)',
|
||||
'perm.action.share_manage': 'إدارة روابط المشاركة',
|
||||
'perm.actionHint.trip_create': 'من يمكنه إنشاء رحلات جديدة',
|
||||
'perm.actionHint.trip_edit':
|
||||
'من يمكنه تغيير اسم الرحلة والتواريخ والوصف والعملة',
|
||||
'perm.actionHint.trip_edit': 'من يمكنه تغيير اسم الرحلة والتواريخ والوصف والعملة',
|
||||
'perm.actionHint.trip_delete': 'من يمكنه حذف رحلة نهائياً',
|
||||
'perm.actionHint.trip_archive': 'من يمكنه أرشفة أو إلغاء أرشفة رحلة',
|
||||
'perm.actionHint.trip_cover_upload': 'من يمكنه رفع أو تغيير صورة الغلاف',
|
||||
'perm.actionHint.member_manage': 'من يمكنه دعوة أو إزالة أعضاء الرحلة',
|
||||
'perm.actionHint.file_upload': 'من يمكنه رفع ملفات إلى رحلة',
|
||||
'perm.actionHint.file_edit': 'من يمكنه تعديل أوصاف الملفات والروابط',
|
||||
'perm.actionHint.file_delete':
|
||||
'من يمكنه نقل الملفات إلى سلة المهملات أو حذفها نهائياً',
|
||||
'perm.actionHint.file_delete': 'من يمكنه نقل الملفات إلى سلة المهملات أو حذفها نهائياً',
|
||||
'perm.actionHint.place_edit': 'من يمكنه إضافة أو تعديل أو حذف الأماكن',
|
||||
'perm.actionHint.day_edit':
|
||||
'من يمكنه تعديل الأيام وملاحظات الأيام وتعيينات الأماكن',
|
||||
'perm.actionHint.day_edit': 'من يمكنه تعديل الأيام وملاحظات الأيام وتعيينات الأماكن',
|
||||
'perm.actionHint.reservation_edit': 'من يمكنه إنشاء أو تعديل أو حذف الحجوزات',
|
||||
'perm.actionHint.budget_edit':
|
||||
'من يمكنه إنشاء أو تعديل أو حذف عناصر الميزانية',
|
||||
'perm.actionHint.budget_edit': 'من يمكنه إنشاء أو تعديل أو حذف عناصر الميزانية',
|
||||
'perm.actionHint.packing_edit': 'من يمكنه إدارة عناصر التعبئة والحقائب',
|
||||
'perm.actionHint.collab_edit':
|
||||
'من يمكنه إنشاء ملاحظات واستطلاعات وإرسال رسائل',
|
||||
'perm.actionHint.collab_edit': 'من يمكنه إنشاء ملاحظات واستطلاعات وإرسال رسائل',
|
||||
'perm.actionHint.share_manage': 'من يمكنه إنشاء أو حذف روابط المشاركة العامة',
|
||||
};
|
||||
export default perm;
|
||||
|
||||
@@ -20,7 +20,6 @@ const photos: TranslationStrings = {
|
||||
'photos.dayLabel': 'اليوم {number}',
|
||||
'photos.photoSelected': 'صورة محددة',
|
||||
'photos.photosSelected': 'صور محددة',
|
||||
'photos.fileTypeHint':
|
||||
'JPG, PNG, WebP · الحد الأقصى 10 ميغابايت · حتى 30 صورة',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · الحد الأقصى 10 ميغابايت · حتى 30 صورة',
|
||||
};
|
||||
export default photos;
|
||||
|
||||
@@ -8,10 +8,8 @@ const places: TranslationStrings = {
|
||||
'استورد ملفات .gpx أو .kml أو .kmz من أدوات مثل Google My Maps وGoogle Earth أو جهاز تتبع GPS.',
|
||||
'places.importFileDropHere': 'انقر لاختيار ملف أو اسحبه وأفلته هنا',
|
||||
'places.importFileDropActive': 'أفلت الملف للاختيار',
|
||||
'places.importFileUnsupported':
|
||||
'نوع الملف غير مدعوم. استخدم .gpx أو .kml أو .kmz.',
|
||||
'places.importFileTooLarge':
|
||||
'الملف كبير جدًا. الحد الأقصى لحجم الرفع هو {maxMb} MB.',
|
||||
'places.importFileUnsupported': 'نوع الملف غير مدعوم. استخدم .gpx أو .kml أو .kmz.',
|
||||
'places.importFileTooLarge': 'الملف كبير جدًا. الحد الأقصى لحجم الرفع هو {maxMb} MB.',
|
||||
'places.importFileError': 'فشل الاستيراد',
|
||||
'places.importAllSkipped': 'جميع الأماكن موجودة بالفعل في الرحلة.',
|
||||
'places.gpxImported': 'تم استيراد {count} مكان من GPX',
|
||||
@@ -29,16 +27,13 @@ const places: TranslationStrings = {
|
||||
'places.kmlKmzImported': 'تم استيراد {count} مكان من KMZ/KML',
|
||||
'places.urlResolved': 'تم استيراد المكان من الرابط',
|
||||
'places.importList': 'استيراد قائمة',
|
||||
'places.kmlKmzSummaryValues':
|
||||
'علامات المواضع: {total} • تم الاستيراد: {created} • تم التجاوز: {skipped}',
|
||||
'places.kmlKmzSummaryValues': 'علامات المواضع: {total} • تم الاستيراد: {created} • تم التجاوز: {skipped}',
|
||||
'places.importGoogleList': 'قائمة Google',
|
||||
'places.importNaverList': 'قائمة Naver',
|
||||
'places.googleListHint':
|
||||
'الصق رابط قائمة Google Maps المشتركة لاستيراد جميع الأماكن.',
|
||||
'places.googleListHint': 'الصق رابط قائمة Google Maps المشتركة لاستيراد جميع الأماكن.',
|
||||
'places.googleListImported': 'تم استيراد {count} أماكن من "{list}"',
|
||||
'places.googleListError': 'فشل استيراد قائمة Google Maps',
|
||||
'places.naverListHint':
|
||||
'الصق رابط قائمة Naver Maps مشتركة لاستيراد جميع الأماكن.',
|
||||
'places.naverListHint': 'الصق رابط قائمة Naver Maps مشتركة لاستيراد جميع الأماكن.',
|
||||
'places.naverListImported': 'تم استيراد {count} مكان من "{list}"',
|
||||
'places.naverListError': 'فشل استيراد قائمة Naver Maps',
|
||||
'places.viewDetails': 'عرض التفاصيل',
|
||||
|
||||
@@ -33,8 +33,7 @@ const planner: TranslationStrings = {
|
||||
'planner.resConfirmed': 'حجز مؤكد · ',
|
||||
'planner.notePlaceholder': 'ملاحظة…',
|
||||
'planner.noteTimePlaceholder': 'الوقت (اختياري)',
|
||||
'planner.noteExamplePlaceholder':
|
||||
'مثال: S3 الساعة 14:30 من المحطة المركزية، عبّارة من الرصيف 7، استراحة غداء…',
|
||||
'planner.noteExamplePlaceholder': 'مثال: S3 الساعة 14:30 من المحطة المركزية، عبّارة من الرصيف 7، استراحة غداء…',
|
||||
'planner.totalCost': 'إجمالي التكلفة',
|
||||
'planner.searchPlaces': 'ابحث عن أماكن…',
|
||||
'planner.allCategories': 'كل الفئات',
|
||||
|
||||
@@ -6,8 +6,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.emptyHint': 'أضف حجوزات للرحلات الجوية والفنادق وغير ذلك',
|
||||
'reservations.add': 'إضافة حجز',
|
||||
'reservations.addManual': 'حجز يدوي',
|
||||
'reservations.placeHint':
|
||||
'نصيحة: يُفضل إنشاء الحجوزات مباشرة من مكان لربطها بخطة اليوم.',
|
||||
'reservations.placeHint': 'نصيحة: يُفضل إنشاء الحجوزات مباشرة من مكان لربطها بخطة اليوم.',
|
||||
'reservations.confirmed': 'مؤكد',
|
||||
'reservations.pending': 'قيد الانتظار',
|
||||
'reservations.summary': '{confirmed} مؤكدة، {pending} قيد الانتظار',
|
||||
@@ -33,8 +32,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.layover.connection': 'رحلة متّصلة',
|
||||
'reservations.layover.layover': 'توقف بيني',
|
||||
'reservations.needsReview': 'مراجعة',
|
||||
'reservations.needsReviewHint':
|
||||
'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.',
|
||||
'reservations.needsReviewHint': 'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.',
|
||||
'reservations.searchLocation': 'ابحث عن محطة، ميناء، عنوان...',
|
||||
'reservations.meta.trainNumber': 'رقم القطار',
|
||||
'reservations.meta.platform': 'المنصة',
|
||||
@@ -98,8 +96,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.budgetCategory': 'فئة الميزانية',
|
||||
'reservations.budgetCategoryPlaceholder': 'مثال: المواصلات، الإقامة',
|
||||
'reservations.budgetCategoryAuto': 'تلقائي (حسب نوع الحجز)',
|
||||
'reservations.budgetHint':
|
||||
'سيتم إنشاء إدخال في الميزانية تلقائيًا عند الحفظ.',
|
||||
'reservations.budgetHint': 'سيتم إنشاء إدخال في الميزانية تلقائيًا عند الحفظ.',
|
||||
'reservations.departureDate': 'المغادرة',
|
||||
'reservations.arrivalDate': 'الوصول',
|
||||
'reservations.departureTime': 'وقت المغادرة',
|
||||
@@ -120,8 +117,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.span.start': 'البداية',
|
||||
'reservations.span.end': 'النهاية',
|
||||
'reservations.span.ongoing': 'جارٍ',
|
||||
'reservations.validation.endBeforeStart':
|
||||
'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء',
|
||||
'reservations.validation.endBeforeStart': 'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء',
|
||||
'reservations.addBooking': 'إضافة حجز',
|
||||
'reservations.import.title': 'استيراد تأكيدات الحجز',
|
||||
'reservations.import.cta': 'استيراد من ملف',
|
||||
@@ -131,46 +127,36 @@ const reservations: TranslationStrings = {
|
||||
'المقبول: EML، PDF، PKPass، HTML، TXT (بحد أقصى 10 ميغابايت لكل ملف، حتى 5 ملفات)',
|
||||
'reservations.import.parsing': 'جارٍ معالجة الملفات…',
|
||||
'reservations.import.previewHeading': 'تم العثور على {count} حجز/حجوزات',
|
||||
'reservations.import.previewEmpty':
|
||||
'تعذّر استخراج أي حجوزات من الملفات المُحمَّلة.',
|
||||
'reservations.import.previewEmpty': 'تعذّر استخراج أي حجوزات من الملفات المُحمَّلة.',
|
||||
'reservations.import.removeItem': 'إزالة',
|
||||
'reservations.import.confirm': 'استيراد {count} حجز/حجوزات',
|
||||
'reservations.import.back': 'رجوع',
|
||||
'reservations.import.success': 'تم استيراد {count} حجز/حجوزات',
|
||||
'reservations.import.partialFailure': 'تم استيراد {created}، فشل {failed}',
|
||||
'reservations.import.error':
|
||||
'فشلت المعالجة. تأكد من أن الملف تأكيد حجز صالح.',
|
||||
'reservations.import.unavailable':
|
||||
'استيراد الحجوزات غير متاح على هذا الخادم.',
|
||||
'reservations.import.unsupportedFormat':
|
||||
'صيغة ملف غير مدعومة. استخدم EML أو PDF أو PKPass أو HTML أو TXT.',
|
||||
'reservations.import.error': 'فشلت المعالجة. تأكد من أن الملف تأكيد حجز صالح.',
|
||||
'reservations.import.unavailable': 'استيراد الحجوزات غير متاح على هذا الخادم.',
|
||||
'reservations.import.unsupportedFormat': 'صيغة ملف غير مدعومة. استخدم EML أو PDF أو PKPass أو HTML أو TXT.',
|
||||
'reservations.import.fileTooLarge': 'الملف "{name}" يتجاوز حد 10 ميغابايت.',
|
||||
'reservations.airtrail.title': 'استيراد من AirTrail',
|
||||
'reservations.airtrail.cta': 'AirTrail',
|
||||
'reservations.airtrail.synced': 'AirTrail',
|
||||
'reservations.airtrail.syncedHint':
|
||||
'متزامن من AirTrail — تبقى التعديلات متزامنة في الاتجاهين.',
|
||||
'reservations.airtrail.syncedHint': 'متزامن من AirTrail — تبقى التعديلات متزامنة في الاتجاهين.',
|
||||
'reservations.airtrail.notSynced': 'غير متزامن',
|
||||
'reservations.airtrail.notSyncedHint':
|
||||
'تمت إزالة هذه الرحلة في AirTrail ولم تعد متزامنة.',
|
||||
'reservations.airtrail.notSyncedHint': 'تمت إزالة هذه الرحلة في AirTrail ولم تعد متزامنة.',
|
||||
'reservations.airtrail.loadError': 'تعذّر تحميل رحلاتك من AirTrail.',
|
||||
'reservations.airtrail.imported': 'تم استيراد {count} رحلة/رحلات',
|
||||
'reservations.airtrail.skippedDuplicate':
|
||||
'{count} موجودة بالفعل في هذه الرحلة، تم تخطّيها',
|
||||
'reservations.airtrail.skippedDuplicate': '{count} موجودة بالفعل في هذه الرحلة، تم تخطّيها',
|
||||
'reservations.airtrail.nothingImported': 'لا شيء لاستيراده.',
|
||||
'reservations.airtrail.importError':
|
||||
'فشل الاستيراد. يُرجى المحاولة مرة أخرى.',
|
||||
'reservations.airtrail.importError': 'فشل الاستيراد. يُرجى المحاولة مرة أخرى.',
|
||||
'reservations.airtrail.undo': 'استيراد من AirTrail',
|
||||
'reservations.airtrail.alreadyImported': 'مُستورَد',
|
||||
'reservations.airtrail.duringTrip': 'خلال هذه الرحلة',
|
||||
'reservations.airtrail.otherFlights': 'رحلات أخرى',
|
||||
'reservations.airtrail.empty':
|
||||
'لم يتم العثور على أي رحلات في حساب AirTrail الخاص بك.',
|
||||
'reservations.airtrail.empty': 'لم يتم العثور على أي رحلات في حساب AirTrail الخاص بك.',
|
||||
'reservations.airtrail.importCta': 'استيراد {count}',
|
||||
'reservations.costsLabel': 'Costs',
|
||||
'reservations.createExpense': 'Create expense',
|
||||
'reservations.createExpenseHint':
|
||||
'Saves the booking, then opens the Costs editor.',
|
||||
'reservations.createExpenseHint': 'Saves the booking, then opens the Costs editor.',
|
||||
'reservations.linkedExpense': 'Linked expense',
|
||||
'reservations.removeExpense': 'Remove expense',
|
||||
};
|
||||
|
||||
@@ -15,8 +15,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mapDefaultHint': 'اتركه فارغًا لاستخدام OpenStreetMap افتراضيًا',
|
||||
'settings.mapHint': 'قالب URL لبلاطات الخريطة',
|
||||
'settings.mapProvider': 'مزود الخريطة',
|
||||
'settings.mapProviderHint':
|
||||
'يؤثر على خرائط Trip Planner و Journey. يستخدم Atlas دائمًا Leaflet.',
|
||||
'settings.mapProviderHint': 'يؤثر على خرائط Trip Planner و Journey. يستخدم Atlas دائمًا Leaflet.',
|
||||
'settings.mapLeafletSubtitle': '2D كلاسيكي، أي بلاطات نقطية',
|
||||
'settings.mapMapboxSubtitle': 'بلاطات متجهية ومبانٍ ثلاثية الأبعاد وتضاريس',
|
||||
'settings.mapExperimental': 'تجريبي',
|
||||
@@ -25,14 +24,11 @@ const settings: TranslationStrings = {
|
||||
'settings.mapMapboxTokenLink': 'mapbox.com ← رموز الوصول',
|
||||
'settings.mapStyle': 'نمط الخريطة',
|
||||
'settings.mapStylePlaceholder': 'اختر نمط Mapbox',
|
||||
'settings.mapStyleHint':
|
||||
'إعداد مسبق أو عنوان URL mapbox://styles/USER/ID خاص بك',
|
||||
'settings.mapStyleHint': 'إعداد مسبق أو عنوان URL mapbox://styles/USER/ID خاص بك',
|
||||
'settings.map3dBuildings': 'مبانٍ ثلاثية الأبعاد وتضاريس',
|
||||
'settings.map3dHint':
|
||||
'إمالة + مبانٍ ثلاثية الأبعاد حقيقية — يعمل مع كل نمط بما في ذلك الأقمار الصناعية.',
|
||||
'settings.map3dHint': 'إمالة + مبانٍ ثلاثية الأبعاد حقيقية — يعمل مع كل نمط بما في ذلك الأقمار الصناعية.',
|
||||
'settings.mapHighQuality': 'وضع الجودة العالية',
|
||||
'settings.mapHighQualityHint':
|
||||
'تحسين الحواف + إسقاط كروي لحواف أكثر حدة وعرض واقعي للعالم.',
|
||||
'settings.mapHighQualityHint': 'تحسين الحواف + إسقاط كروي لحواف أكثر حدة وعرض واقعي للعالم.',
|
||||
'settings.mapHighQualityWarning': 'قد يؤثر على الأداء في الأجهزة الأقل قدرة.',
|
||||
'settings.mapTipLabel': 'نصيحة:',
|
||||
'settings.mapTip':
|
||||
@@ -57,8 +53,7 @@ const settings: TranslationStrings = {
|
||||
'settings.temperature': 'وحدة الحرارة',
|
||||
'settings.timeFormat': 'تنسيق الوقت',
|
||||
'settings.bookingLabels': 'تسميات مسارات الحجوزات',
|
||||
'settings.bookingLabelsHint':
|
||||
'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
|
||||
'settings.bookingLabelsHint': 'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
|
||||
'settings.blurBookingCodes': 'إخفاء رموز الحجز',
|
||||
'settings.optimizeFromAccommodation': 'تحسين المسار انطلاقًا من مكان الإقامة',
|
||||
'settings.optimizeFromAccommodationHint':
|
||||
@@ -77,8 +72,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.noChannels':
|
||||
'لم يتم تكوين قنوات إشعارات. اطلب من المسؤول إعداد إشعارات البريد الإلكتروني أو webhook.',
|
||||
'settings.webhookUrl.label': 'رابط Webhook',
|
||||
'settings.webhookUrl.hint':
|
||||
'أدخل رابط Webhook الخاص بـ Discord أو Slack أو المخصص لتلقي الإشعارات.',
|
||||
'settings.webhookUrl.hint': 'أدخل رابط Webhook الخاص بـ Discord أو Slack أو المخصص لتلقي الإشعارات.',
|
||||
'settings.webhookUrl.saved': 'تم حفظ رابط Webhook',
|
||||
'settings.webhookUrl.test': 'اختبار',
|
||||
'settings.webhookUrl.testSuccess': 'تم إرسال Webhook الاختباري بنجاح',
|
||||
@@ -94,11 +88,9 @@ const settings: TranslationStrings = {
|
||||
'settings.ntfyUrl.testSuccess': 'تم إرسال إشعار Ntfy التجريبي بنجاح',
|
||||
'settings.ntfyUrl.testFailed': 'فشل إشعار Ntfy التجريبي',
|
||||
'settings.ntfyUrl.tokenCleared': 'تم مسح رمز الوصول',
|
||||
'settings.notificationsDisabled':
|
||||
'الإشعارات غير مكوّنة. اطلب من المسؤول تفعيل إشعارات البريد الإلكتروني أو Webhook.',
|
||||
'settings.notificationsDisabled': 'الإشعارات غير مكوّنة. اطلب من المسؤول تفعيل إشعارات البريد الإلكتروني أو Webhook.',
|
||||
'settings.notificationsActive': 'القناة النشطة',
|
||||
'settings.notificationsManagedByAdmin':
|
||||
'يتم تكوين أحداث الإشعارات بواسطة المسؤول.',
|
||||
'settings.notificationsManagedByAdmin': 'يتم تكوين أحداث الإشعارات بواسطة المسؤول.',
|
||||
'settings.on': 'تشغيل',
|
||||
'settings.off': 'إيقاف',
|
||||
'settings.mcp.title': 'إعداد MCP',
|
||||
@@ -116,17 +108,14 @@ const settings: TranslationStrings = {
|
||||
'settings.mcp.tokenCreatedAt': 'أُنشئ',
|
||||
'settings.mcp.tokenUsedAt': 'استُخدم',
|
||||
'settings.mcp.deleteTokenTitle': 'حذف الرمز',
|
||||
'settings.mcp.deleteTokenMessage':
|
||||
'سيتوقف هذا الرمز عن العمل فوراً. أي عميل MCP يستخدمه سيفقد الوصول.',
|
||||
'settings.mcp.deleteTokenMessage': 'سيتوقف هذا الرمز عن العمل فوراً. أي عميل MCP يستخدمه سيفقد الوصول.',
|
||||
'settings.mcp.modal.createTitle': 'إنشاء رمز API',
|
||||
'settings.mcp.modal.tokenName': 'اسم الرمز',
|
||||
'settings.mcp.modal.tokenNamePlaceholder':
|
||||
'مثال: Claude Desktop، حاسوب العمل',
|
||||
'settings.mcp.modal.tokenNamePlaceholder': 'مثال: Claude Desktop، حاسوب العمل',
|
||||
'settings.mcp.modal.creating': 'جارٍ الإنشاء…',
|
||||
'settings.mcp.modal.create': 'إنشاء الرمز',
|
||||
'settings.mcp.modal.createdTitle': 'تم إنشاء الرمز',
|
||||
'settings.mcp.modal.createdWarning':
|
||||
'سيُعرض هذا الرمز مرة واحدة فقط. انسخه واحفظه الآن — لا يمكن استرداده.',
|
||||
'settings.mcp.modal.createdWarning': 'سيُعرض هذا الرمز مرة واحدة فقط. انسخه واحفظه الآن — لا يمكن استرداده.',
|
||||
'settings.mcp.modal.done': 'تم',
|
||||
'settings.mcp.toast.created': 'تم إنشاء الرمز',
|
||||
'settings.mcp.toast.createError': 'فشل إنشاء الرمز',
|
||||
@@ -157,16 +146,13 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.sessionExpires': 'تنتهي',
|
||||
'settings.oauth.revoke': 'إلغاء',
|
||||
'settings.oauth.revokeSession': 'إلغاء الجلسة',
|
||||
'settings.oauth.revokeSessionMessage':
|
||||
'سيؤدي هذا إلى إلغاء الوصول لهذه الجلسة OAuth فوراً.',
|
||||
'settings.oauth.revokeSessionMessage': 'سيؤدي هذا إلى إلغاء الوصول لهذه الجلسة OAuth فوراً.',
|
||||
'settings.oauth.modal.createTitle': 'تسجيل عميل OAuth',
|
||||
'settings.oauth.modal.presets': 'إعدادات سريعة',
|
||||
'settings.oauth.modal.clientName': 'اسم التطبيق',
|
||||
'settings.oauth.modal.clientNamePlaceholder':
|
||||
'مثال: Claude Web، تطبيق MCP الخاص بي',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'مثال: Claude Web، تطبيق MCP الخاص بي',
|
||||
'settings.oauth.modal.redirectUris': 'عناوين URI لإعادة التوجيه',
|
||||
'settings.oauth.modal.redirectUrisHint':
|
||||
'عنوان URI واحد لكل سطر. يُطلب HTTPS (localhost مستثنى). يُطبق تطابق دقيق.',
|
||||
'settings.oauth.modal.redirectUrisHint': 'عنوان URI واحد لكل سطر. يُطلب HTTPS (localhost مستثنى). يُطبق تطابق دقيق.',
|
||||
'settings.oauth.modal.scopes': 'النطاقات المسموح بها',
|
||||
'settings.oauth.modal.scopesHint':
|
||||
'list_trips وget_trip_summary متاحان دائماً — لا يُطلب نطاق. يساعدان الذكاء الاصطناعي في اكتشاف معرّفات الرحلات.',
|
||||
@@ -175,16 +161,14 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.modal.creating': 'جارٍ التسجيل…',
|
||||
'settings.oauth.modal.create': 'تسجيل العميل',
|
||||
'settings.oauth.modal.createdTitle': 'تم تسجيل العميل',
|
||||
'settings.oauth.modal.createdWarning':
|
||||
'يُعرض سر العميل مرة واحدة فقط. انسخه الآن — لا يمكن استرداده.',
|
||||
'settings.oauth.modal.createdWarning': 'يُعرض سر العميل مرة واحدة فقط. انسخه الآن — لا يمكن استرداده.',
|
||||
'settings.oauth.toast.createError': 'فشل تسجيل عميل OAuth',
|
||||
'settings.oauth.toast.deleted': 'تم حذف عميل OAuth',
|
||||
'settings.oauth.toast.deleteError': 'فشل حذف عميل OAuth',
|
||||
'settings.oauth.toast.revoked': 'تم إلغاء الجلسة',
|
||||
'settings.oauth.toast.revokeError': 'فشل إلغاء الجلسة',
|
||||
'settings.oauth.toast.rotateError': 'فشل تجديد سر العميل',
|
||||
'settings.oauth.modal.machineClient':
|
||||
'عميل آلي (بدون تسجيل دخول عبر المتصفح)',
|
||||
'settings.oauth.modal.machineClient': 'عميل آلي (بدون تسجيل دخول عبر المتصفح)',
|
||||
'settings.oauth.modal.machineClientHint':
|
||||
'استخدام منحة client_credentials — لا تحتاج إلى عناوين إعادة التوجيه. يُصدر الرمز المميز مباشرةً عبر client_id + client_secret ويعمل بصلاحياتك ضمن النطاقات المحددة.',
|
||||
'settings.oauth.modal.machineClientUsage':
|
||||
@@ -221,19 +205,15 @@ const settings: TranslationStrings = {
|
||||
'settings.passwordRequired': 'أدخل كلمة المرور الحالية والجديدة',
|
||||
'settings.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل',
|
||||
'settings.passwordMismatch': 'كلمتا المرور غير متطابقتين',
|
||||
'settings.passwordWeak':
|
||||
'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم ورمز خاص',
|
||||
'settings.passwordWeak': 'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم ورمز خاص',
|
||||
'settings.passwordChanged': 'تم تغيير كلمة المرور بنجاح',
|
||||
'settings.mustChangePassword':
|
||||
'يجب عليك تغيير كلمة المرور قبل المتابعة. يرجى تعيين كلمة مرور جديدة أدناه.',
|
||||
'settings.mustChangePassword': 'يجب عليك تغيير كلمة المرور قبل المتابعة. يرجى تعيين كلمة مرور جديدة أدناه.',
|
||||
'settings.deleteAccount': 'حذف الحساب',
|
||||
'settings.deleteAccountTitle': 'هل تريد حذف حسابك؟',
|
||||
'settings.deleteAccountWarning':
|
||||
'سيتم حذف حسابك وجميع رحلاتك وأماكنك وملفاتك نهائيًا. لا يمكن التراجع عن ذلك.',
|
||||
'settings.deleteAccountWarning': 'سيتم حذف حسابك وجميع رحلاتك وأماكنك وملفاتك نهائيًا. لا يمكن التراجع عن ذلك.',
|
||||
'settings.deleteAccountConfirm': 'حذف نهائي',
|
||||
'settings.deleteBlockedTitle': 'الحذف غير ممكن',
|
||||
'settings.deleteBlockedMessage':
|
||||
'أنت المسؤول الوحيد. قم بترقية مستخدم آخر إلى مسؤول قبل حذف حسابك.',
|
||||
'settings.deleteBlockedMessage': 'أنت المسؤول الوحيد. قم بترقية مستخدم آخر إلى مسؤول قبل حذف حسابك.',
|
||||
'settings.roleUser': 'مستخدم',
|
||||
'settings.saveProfile': 'حفظ الملف الشخصي',
|
||||
'settings.toast.mapSaved': 'تم حفظ إعدادات الخريطة',
|
||||
@@ -248,13 +228,10 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.title': 'المصادقة الثنائية (2FA)',
|
||||
'settings.mfa.description':
|
||||
'تضيف خطوة ثانية عند تسجيل الدخول. استخدم تطبيق مصادقة (Google Authenticator، Authy، إلخ).',
|
||||
'settings.mfa.requiredByPolicy':
|
||||
'المسؤول يتطلب المصادقة الثنائية. اضبط تطبيق المصادقة أدناه قبل المتابعة.',
|
||||
'settings.mfa.requiredByPolicy': 'المسؤول يتطلب المصادقة الثنائية. اضبط تطبيق المصادقة أدناه قبل المتابعة.',
|
||||
'settings.mfa.backupTitle': 'رموز النسخ الاحتياطي',
|
||||
'settings.mfa.backupDescription':
|
||||
'استخدم هذه الرموز لمرة واحدة إذا فقدت الوصول إلى تطبيق المصادقة.',
|
||||
'settings.mfa.backupWarning':
|
||||
'احفظ هذه الرموز الآن. كل رمز يمكن استخدامه مرة واحدة فقط.',
|
||||
'settings.mfa.backupDescription': 'استخدم هذه الرموز لمرة واحدة إذا فقدت الوصول إلى تطبيق المصادقة.',
|
||||
'settings.mfa.backupWarning': 'احفظ هذه الرموز الآن. كل رمز يمكن استخدامه مرة واحدة فقط.',
|
||||
'settings.mfa.backupCopy': 'نسخ الرموز',
|
||||
'settings.mfa.backupDownload': 'تنزيل TXT',
|
||||
'settings.mfa.backupPrint': 'طباعة / PDF',
|
||||
@@ -274,8 +251,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.toastDisabled': 'تم تعطيل المصادقة الثنائية',
|
||||
'settings.mfa.demoBlocked': 'غير متاح في الوضع التجريبي',
|
||||
'settings.tabs.offline': 'Offline', // en-fallback
|
||||
'settings.mapTemplatePlaceholder':
|
||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // en-fallback
|
||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // en-fallback
|
||||
'settings.notificationPreferences.email': 'Email', // en-fallback
|
||||
'settings.notificationPreferences.webhook': 'Webhook', // 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.ntfyUrl.topicPlaceholder': 'my-trek-alerts', // en-fallback
|
||||
'settings.ntfyUrl.serverPlaceholder': 'https://ntfy.sh', // en-fallback
|
||||
'settings.oauth.modal.redirectUrisPlaceholder':
|
||||
'https://your-app.com/callback\nhttps://your-app.com/auth', // en-fallback
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': '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.lostLuggageVip': 'Lost Luggage VIP', // en-fallback
|
||||
'settings.about.supporter.tier.businessClassDreamer':
|
||||
'Business Class Dreamer', // en-fallback
|
||||
'settings.about.supporter.tier.businessClassDreamer': 'Business Class Dreamer', // en-fallback
|
||||
'settings.about.supporter.tier.budgetTraveller': 'Budget Traveller', // en-fallback
|
||||
'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate', // en-fallback
|
||||
"settings.currency": "Currency",
|
||||
"settings.currencyHint": "All amounts in Costs are converted to and shown in this currency.",
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.passkey.title': 'مفاتيح المرور',
|
||||
'settings.passkey.description':
|
||||
'سجّل الدخول بشكل أسرع وأكثر مقاومة للتصيّد باستخدام مفتاح مرور — ببصمة إصبعك أو وجهك أو رمز PIN أو مفتاح أمان مادي. تبقى كلمة المرور كنسخة احتياطية.',
|
||||
@@ -300,8 +274,7 @@ const settings: TranslationStrings = {
|
||||
'مفاتيح المرور مفعّلة لكنها لم تُهيّأ بالكامل على هذا الخادم بعد. اطلب من المسؤول تعيين نطاق WebAuthn.',
|
||||
'settings.passkey.add': 'إضافة مفتاح مرور',
|
||||
'settings.passkey.addTitle': 'إضافة مفتاح مرور',
|
||||
'settings.passkey.passwordPrompt':
|
||||
'أكّد كلمة المرور الحالية، ثم اتبع التعليمات على جهازك.',
|
||||
'settings.passkey.passwordPrompt': 'أكّد كلمة المرور الحالية، ثم اتبع التعليمات على جهازك.',
|
||||
'settings.passkey.passwordRequired': 'كلمة المرور الحالية مطلوبة.',
|
||||
'settings.passkey.namePlaceholder': 'الاسم (اختياري، مثل "iPhone")',
|
||||
'settings.passkey.addedToast': 'تمت إضافة مفتاح المرور',
|
||||
@@ -309,8 +282,7 @@ const settings: TranslationStrings = {
|
||||
'settings.passkey.addError': 'تعذّرت إضافة مفتاح المرور',
|
||||
'settings.passkey.cancelled': 'تم إلغاء إعداد مفتاح المرور',
|
||||
'settings.passkey.deleted': 'تمت إزالة مفتاح المرور',
|
||||
'settings.passkey.deleteConfirm':
|
||||
'إزالة مفتاح المرور هذا؟ أكّد بكلمة المرور الخاصة بك.',
|
||||
'settings.passkey.deleteConfirm': 'إزالة مفتاح المرور هذا؟ أكّد بكلمة المرور الخاصة بك.',
|
||||
'settings.passkey.rename': 'إعادة التسمية',
|
||||
'settings.passkey.defaultName': 'مفتاح المرور',
|
||||
'settings.passkey.synced': 'متزامن',
|
||||
@@ -318,9 +290,11 @@ const settings: TranslationStrings = {
|
||||
'settings.passkey.lastUsed': 'آخر استخدام',
|
||||
'settings.passkey.neverUsed': 'لم يُستخدم قط',
|
||||
'settings.mapPoiPill': 'استكشاف الأماكن على الخريطة',
|
||||
'settings.mapPoiPillHint': 'أظهر شريط فئات على خريطة الرحلة للعثور على المطاعم والفنادق والمزيد القريبة من OpenStreetMap.',
|
||||
'settings.mapPoiPillHint':
|
||||
'أظهر شريط فئات على خريطة الرحلة للعثور على المطاعم والفنادق والمزيد القريبة من OpenStreetMap.',
|
||||
'settings.airtrail.title': 'AirTrail',
|
||||
'settings.airtrail.hint': 'اربط نسخة AirTrail المُستضافة ذاتيًا لاستيراد الرحلات ومزامنتها. أنشئ مفتاح API في AirTrail ضمن الإعدادات ← الأمان.',
|
||||
'settings.airtrail.hint':
|
||||
'اربط نسخة AirTrail المُستضافة ذاتيًا لاستيراد الرحلات ومزامنتها. أنشئ مفتاح API في AirTrail ضمن الإعدادات ← الأمان.',
|
||||
'settings.airtrail.url': 'رابط النسخة',
|
||||
'settings.airtrail.apiKey': 'مفتاح API',
|
||||
'settings.airtrail.apiKeyPlaceholder': 'مفتاح API من نوع Bearer',
|
||||
@@ -328,7 +302,8 @@ const settings: TranslationStrings = {
|
||||
'settings.airtrail.allowInsecureTls': 'السماح بالشهادات الموقّعة ذاتيًا',
|
||||
'settings.airtrail.allowInsecureTlsHint': 'فعّل هذا فقط لنسخة موثوقة على شبكتك الخاصة.',
|
||||
'settings.airtrail.writeBack': 'كتابة التغييرات إلى AirTrail',
|
||||
'settings.airtrail.writeBackHint': 'مُعطّل افتراضيًا: AirTrail هو مصدر الحقيقة وTREK يقرأ منه فقط. فعّله لإرسال التعديلات التي تجريها في TREK إلى AirTrail.',
|
||||
'settings.airtrail.writeBackHint':
|
||||
'مُعطّل افتراضيًا: AirTrail هو مصدر الحقيقة وTREK يقرأ منه فقط. فعّله لإرسال التعديلات التي تجريها في TREK إلى AirTrail.',
|
||||
'settings.airtrail.connected': 'متصل',
|
||||
'settings.airtrail.notConnected': 'غير متصل',
|
||||
'settings.airtrail.toast.saved': 'تم حفظ اتصال AirTrail',
|
||||
|
||||
@@ -5,22 +5,18 @@ const system_notice: TranslationStrings = {
|
||||
'system_notice.v3_photos.body':
|
||||
'تمت إزالة تبويب **الصور** من مخطط الرحلة. صورك آمنة — لم يعدّل TREK مكتبتك على Immich أو Synology قطّ.\n\nتعيش الصور الآن في إضافة **Journey**. Journey اختيارية — إن لم تكن متاحة بعد، اطلب من المسؤول تفعيلها عبر Admin ← الإضافات.',
|
||||
'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.highlight_timeline': 'جدول زمني يومي ومعرض',
|
||||
'system_notice.v3_journey.highlight_photos': 'استيراد من Immich أو Synology',
|
||||
'system_notice.v3_journey.highlight_share': 'مشاركة علنية — دون تسجيل دخول',
|
||||
'system_notice.v3_journey.highlight_export': 'تصدير كألبوم صور PDF',
|
||||
'system_notice.v3_features.title': 'مزيد من مميزات 3.0',
|
||||
'system_notice.v3_features.body':
|
||||
'بعض الجديد الآخر الجدير بالمعرفة في هذا الإصدار.',
|
||||
'system_notice.v3_features.highlight_dashboard':
|
||||
'إعادة تصميم لوحة التحكم mobile-first',
|
||||
'system_notice.v3_features.body': 'بعض الجديد الآخر الجدير بالمعرفة في هذا الإصدار.',
|
||||
'system_notice.v3_features.highlight_dashboard': 'إعادة تصميم لوحة التحكم mobile-first',
|
||||
'system_notice.v3_features.highlight_offline': 'وضع لا اتصال كامل كتطبيق PWA',
|
||||
'system_notice.v3_features.highlight_search': 'إكمال تلقائي في الوقت الفعلي',
|
||||
'system_notice.v3_features.highlight_import':
|
||||
'استيراد أماكن من ملفات KMZ/KML',
|
||||
'system_notice.v3_features.highlight_import': 'استيراد أماكن من ملفات KMZ/KML',
|
||||
'system_notice.v3_mcp.title': 'MCP: ترقية OAuth 2.1',
|
||||
'system_notice.v3_mcp.body':
|
||||
'تمت إعادة تصميم تكامل MCP بالكامل. OAuth 2.1 هو الآن طريقة المصادقة الموصى بها. الرموز الثابتة (trek_…) مهملة وستُزال في إصدار مستقبلي.',
|
||||
@@ -31,8 +27,7 @@ const system_notice: TranslationStrings = {
|
||||
'system_notice.v3_thankyou.title': 'كلمة شخصية مني',
|
||||
'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) يبقي الأضواء مشتعلة.',
|
||||
'system_notice.v3014_whitespace_collision.title':
|
||||
'إجراء مطلوب: تعارض في حسابات المستخدمين',
|
||||
'system_notice.v3014_whitespace_collision.title': 'إجراء مطلوب: تعارض في حسابات المستخدمين',
|
||||
'system_notice.v3014_whitespace_collision.body':
|
||||
'اكتشف ترقية 3.0.14 تعارضًا في أسماء مستخدمين أو بريد إلكتروني ناتجًا عن مسافات بيضاء في بداية أو نهاية القيم المخزنة. تمت إعادة تسمية الحسابات المتأثرة تلقائيًا. تحقق من سجلات الخادم بحثًا عن أسطر تبدأ بـ **[migration] WHITESPACE COLLISION** لتحديد الحسابات التي تحتاج إلى مراجعة.',
|
||||
'system_notice.welcome_v1.title': 'مرحبًا بك في TREK',
|
||||
|
||||
@@ -9,7 +9,7 @@ const trip: TranslationStrings = {
|
||||
'trip.tabs.packingShort': 'تجهيز',
|
||||
'trip.tabs.lists': 'القوائم',
|
||||
'trip.tabs.listsShort': 'القوائم',
|
||||
'trip.tabs.budget': "Costs",
|
||||
'trip.tabs.budget': 'Costs',
|
||||
'trip.tabs.files': 'الملفات',
|
||||
'trip.loading': 'جارٍ تحميل الرحلة...',
|
||||
'trip.loadingPhotos': 'جارٍ تحميل صور الأماكن...',
|
||||
|
||||
@@ -11,7 +11,6 @@ const trips: TranslationStrings = {
|
||||
'trips.reminderDays': 'أيام',
|
||||
'trips.reminderCustom': 'مخصص',
|
||||
'trips.reminderDaysBefore': 'أيام قبل المغادرة',
|
||||
'trips.reminderDisabledHint':
|
||||
'تذكيرات الرحلة معطلة. قم بتفعيلها من الإدارة > الإعدادات > الإشعارات.',
|
||||
'trips.reminderDisabledHint': 'تذكيرات الرحلة معطلة. قم بتفعيلها من الإدارة > الإعدادات > الإشعارات.',
|
||||
};
|
||||
export default trips;
|
||||
|
||||
@@ -8,8 +8,7 @@ const vacay: TranslationStrings = {
|
||||
'vacay.addPrevYear': 'إضافة السنة السابقة',
|
||||
'vacay.removeYear': 'إزالة السنة',
|
||||
'vacay.removeYearConfirm': 'إزالة {year}؟',
|
||||
'vacay.removeYearHint':
|
||||
'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.',
|
||||
'vacay.removeYearHint': 'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.',
|
||||
'vacay.remove': 'إزالة',
|
||||
'vacay.persons': 'الأشخاص',
|
||||
'vacay.noPersons': 'لم تتم إضافة أشخاص بعد',
|
||||
@@ -57,8 +56,7 @@ const vacay: TranslationStrings = {
|
||||
'vacay.weekStart': 'يبدأ الأسبوع في',
|
||||
'vacay.weekStartHint': 'اختر ما إذا كان الأسبوع يبدأ يوم الاثنين أو الأحد',
|
||||
'vacay.carryOver': 'الترحيل',
|
||||
'vacay.carryOverHint':
|
||||
'ترحيل أيام الإجازة المتبقية تلقائيًا إلى السنة التالية',
|
||||
'vacay.carryOverHint': 'ترحيل أيام الإجازة المتبقية تلقائيًا إلى السنة التالية',
|
||||
'vacay.sharing': 'المشاركة',
|
||||
'vacay.sharingHint': 'شارك خطة إجازاتك مع مستخدمي TREK الآخرين',
|
||||
'vacay.owner': 'المالك',
|
||||
@@ -90,7 +88,6 @@ const vacay: TranslationStrings = {
|
||||
'vacay.fuseInfo2': 'يمكن لكلا الطرفين إنشاء وتعديل الإدخالات لبعضهما البعض.',
|
||||
'vacay.fuseInfo3': 'يمكن لكلا الطرفين حذف الإدخالات وتغيير مستحقات الإجازة.',
|
||||
'vacay.fuseInfo4': 'تتم مشاركة الإعدادات مثل العطل الرسمية وعطل الشركة.',
|
||||
'vacay.fuseInfo5':
|
||||
'يمكن فك الدمج في أي وقت من قبل أي طرف. ستبقى إدخالاتك محفوظة.',
|
||||
'vacay.fuseInfo5': 'يمكن فك الدمج في أي وقت من قبل أي طرف. ستبقى إدخالاتك محفوظة.',
|
||||
};
|
||||
export default vacay;
|
||||
|
||||
+52
-98
@@ -2,22 +2,19 @@ import type { TranslationStrings } from '../types';
|
||||
|
||||
const admin: TranslationStrings = {
|
||||
'admin.notifications.title': 'Notificações',
|
||||
'admin.notifications.hint':
|
||||
'Escolha um canal de notificação. Apenas um pode estar ativo por vez.',
|
||||
'admin.notifications.hint': 'Escolha um canal de notificação. Apenas um pode estar ativo por vez.',
|
||||
'admin.notifications.none': 'Desativado',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.save': 'Salvar configurações de notificação',
|
||||
'admin.notifications.saved': 'Configurações de notificação salvas',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de teste',
|
||||
'admin.notifications.testWebhookSuccess':
|
||||
'Webhook de teste enviado com sucesso',
|
||||
'admin.notifications.testWebhookSuccess': 'Webhook de teste enviado com sucesso',
|
||||
'admin.notifications.testWebhookFailed': 'Falha ao enviar webhook de teste',
|
||||
'admin.smtp.title': 'E-mail e notificações',
|
||||
'admin.smtp.hint': 'Configuração SMTP para envio de notificações por e-mail.',
|
||||
'admin.smtp.testButton': 'Enviar e-mail de teste',
|
||||
'admin.webhook.hint':
|
||||
'Enviar notificações para um webhook externo (Discord, Slack, etc.).',
|
||||
'admin.webhook.hint': 'Enviar notificações para um webhook externo (Discord, Slack, etc.).',
|
||||
'admin.smtp.testSuccess': 'E-mail de teste enviado com sucesso',
|
||||
'admin.smtp.testFailed': 'Falha ao enviar e-mail de teste',
|
||||
'admin.title': 'Administração',
|
||||
@@ -40,8 +37,7 @@ const admin: TranslationStrings = {
|
||||
'admin.editUser': 'Editar usuário',
|
||||
'admin.newPassword': 'Nova senha',
|
||||
'admin.newPasswordHint': 'Deixe em branco para manter a senha atual',
|
||||
'admin.deleteUser':
|
||||
'Excluir o usuário "{name}"? Todas as viagens serão excluídas permanentemente.',
|
||||
'admin.deleteUser': 'Excluir o usuário "{name}"? Todas as viagens serão excluídas permanentemente.',
|
||||
'admin.deleteUserTitle': 'Excluir usuário',
|
||||
'admin.newPasswordPlaceholder': 'Digite a nova senha…',
|
||||
'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.userCreated': 'Usuário criado',
|
||||
'admin.toast.createError': 'Falha ao criar usuário',
|
||||
'admin.toast.fieldsRequired':
|
||||
'Nome de usuário, e-mail e senha são obrigatórios',
|
||||
'admin.toast.fieldsRequired': 'Nome de usuário, e-mail e senha são obrigatórios',
|
||||
'admin.createUser': 'Criar usuário',
|
||||
'admin.invite.title': 'Links de convite',
|
||||
'admin.invite.subtitle': 'Crie links de cadastro de uso único',
|
||||
@@ -80,51 +75,40 @@ const admin: TranslationStrings = {
|
||||
'admin.passwordLogin': 'Password Login',
|
||||
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
|
||||
'admin.passwordRegistration': 'Password Registration',
|
||||
'admin.passwordRegistrationHint':
|
||||
'Allow new users to register with email and password',
|
||||
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
|
||||
'admin.oidcLogin': 'SSO Login',
|
||||
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
|
||||
'admin.oidcRegistration': 'SSO Auto-Provisioning',
|
||||
'admin.oidcRegistrationHint':
|
||||
'Automatically create accounts for new SSO users',
|
||||
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
|
||||
'admin.envOverrideHint':
|
||||
'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.requireMfa': 'Exigir autenticação em dois fatores (2FA)',
|
||||
'admin.requireMfaHint':
|
||||
'Usuários sem 2FA precisam concluir a configuração em Configurações antes de usar o app.',
|
||||
'admin.requireMfaHint': 'Usuários sem 2FA precisam concluir a configuração em Configurações antes de usar o app.',
|
||||
'admin.apiKeys': 'Chaves de API',
|
||||
'admin.apiKeysHint':
|
||||
'Opcional. Habilita dados estendidos de lugares, como fotos e clima.',
|
||||
'admin.apiKeysHint': 'Opcional. Habilita dados estendidos de lugares, como fotos e clima.',
|
||||
'admin.mapsKey': 'Chave da API Google Maps',
|
||||
'admin.mapsKeyHint':
|
||||
'Necessária para busca de lugares. Obtenha em console.cloud.google.com',
|
||||
'admin.mapsKeyHint': 'Necessária para busca de lugares. Obtenha em console.cloud.google.com',
|
||||
'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.',
|
||||
'admin.recommended': 'Recomendado',
|
||||
'admin.weatherKey': 'Chave OpenWeatherMap',
|
||||
'admin.weatherKeyHint':
|
||||
'Para dados meteorológicos. Grátis em openweathermap.org',
|
||||
'admin.weatherKeyHint': 'Para dados meteorológicos. Grátis em openweathermap.org',
|
||||
'admin.validateKey': 'Testar',
|
||||
'admin.keyValid': 'Conectado',
|
||||
'admin.keyInvalid': 'Inválida',
|
||||
'admin.keySaved': 'Chaves de API salvas',
|
||||
'admin.oidcTitle': 'Login Único (OIDC)',
|
||||
'admin.oidcSubtitle':
|
||||
'Permitir login via provedores externos como Google, Apple, Authentik ou Keycloak.',
|
||||
'admin.oidcSubtitle': 'Permitir login via provedores externos como Google, Apple, Authentik ou Keycloak.',
|
||||
'admin.oidcDisplayName': 'Nome exibido',
|
||||
'admin.oidcIssuer': 'URL do emissor',
|
||||
'admin.oidcIssuerHint':
|
||||
'URL do emissor OpenID Connect do provedor, ex.: https://accounts.google.com',
|
||||
'admin.oidcIssuerHint': 'URL do emissor OpenID Connect do provedor, ex.: https://accounts.google.com',
|
||||
'admin.oidcSaved': 'Configuração OIDC salva',
|
||||
'admin.oidcOnlyMode': 'Desativar login por senha',
|
||||
'admin.oidcOnlyModeHint':
|
||||
'Quando ativado, só é permitido login SSO. Login e cadastro por senha ficam bloqueados.',
|
||||
'admin.oidcOnlyModeHint': 'Quando ativado, só é permitido login SSO. Login e cadastro por senha ficam bloqueados.',
|
||||
'admin.fileTypes': 'Tipos de arquivo permitidos',
|
||||
'admin.fileTypesHint':
|
||||
'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.fileTypesHint': '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.fileTypesSaved': 'Configurações de tipos de arquivo salvas',
|
||||
'admin.placesPhotos.title': 'Fotos de Locais',
|
||||
'admin.placesPhotos.subtitle':
|
||||
@@ -136,8 +120,7 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle':
|
||||
'Ativar peso e atribuição de mala para itens da lista',
|
||||
'admin.bagTracking.subtitle': 'Ativar peso e atribuição de mala para itens da lista',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
'admin.collab.chat.subtitle': 'Mensagens em tempo real para colaboração',
|
||||
'admin.collab.notes.title': 'Notas',
|
||||
@@ -145,8 +128,7 @@ const admin: TranslationStrings = {
|
||||
'admin.collab.polls.title': 'Enquetes',
|
||||
'admin.collab.polls.subtitle': 'Enquetes e votações em grupo',
|
||||
'admin.collab.whatsnext.title': 'Próximos passos',
|
||||
'admin.collab.whatsnext.subtitle':
|
||||
'Sugestões de atividades e próximos passos',
|
||||
'admin.collab.whatsnext.subtitle': 'Sugestões de atividades e próximos passos',
|
||||
'admin.tabs.config': 'Personalização',
|
||||
'admin.tabs.defaults': 'Padrões 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.tabs.templates': 'Modelos de mala',
|
||||
'admin.packingTemplates.title': 'Modelos de mala',
|
||||
'admin.packingTemplates.subtitle':
|
||||
'Crie listas de mala reutilizáveis para suas viagens',
|
||||
'admin.packingTemplates.subtitle': 'Crie listas de mala reutilizáveis para suas viagens',
|
||||
'admin.packingTemplates.create': 'Novo modelo',
|
||||
'admin.packingTemplates.namePlaceholder': 'Nome do modelo (ex.: Praia)',
|
||||
'admin.packingTemplates.empty': 'Nenhum modelo criado ainda',
|
||||
@@ -176,34 +157,24 @@ const admin: TranslationStrings = {
|
||||
'admin.packingTemplates.saveError': 'Falha ao salvar',
|
||||
'admin.tabs.addons': 'Complementos',
|
||||
'admin.addons.title': 'Complementos',
|
||||
'admin.addons.subtitle':
|
||||
'Ative ou desative recursos para personalizar sua experiência no TREK.',
|
||||
'admin.addons.subtitle': 'Ative ou desative recursos para personalizar sua experiência no TREK.',
|
||||
'admin.addons.catalog.memories.name': 'Memórias',
|
||||
'admin.addons.catalog.memories.description':
|
||||
'Álbuns de fotos compartilhados em cada viagem',
|
||||
'admin.addons.catalog.memories.description': 'Álbuns de fotos compartilhados em cada viagem',
|
||||
'admin.addons.catalog.packing.name': 'Listas',
|
||||
'admin.addons.catalog.packing.description':
|
||||
'Listas de bagagem e tarefas a fazer para suas viagens',
|
||||
'admin.addons.catalog.packing.description': 'Listas de bagagem e tarefas a fazer para suas viagens',
|
||||
'admin.addons.catalog.budget.name': 'Orçamento',
|
||||
'admin.addons.catalog.budget.description':
|
||||
'Acompanhe despesas e planeje o orçamento da viagem',
|
||||
'admin.addons.catalog.budget.description': 'Acompanhe despesas e planeje o orçamento da viagem',
|
||||
'admin.addons.catalog.documents.name': 'Documentos',
|
||||
'admin.addons.catalog.documents.description':
|
||||
'Armazene e gerencie documentos de viagem',
|
||||
'admin.addons.catalog.documents.description': 'Armazene e gerencie documentos de viagem',
|
||||
'admin.addons.catalog.vacay.name': 'Férias',
|
||||
'admin.addons.catalog.vacay.description':
|
||||
'Planejador de férias pessoal com visão em calendário',
|
||||
'admin.addons.catalog.vacay.description': 'Planejador de férias pessoal com visão em calendário',
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description':
|
||||
'Mapa mundial com países visitados e estatísticas',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa mundial com países visitados e estatísticas',
|
||||
'admin.addons.catalog.collab.name': 'Colab',
|
||||
'admin.addons.catalog.collab.description':
|
||||
'Notas, enquetes e chat em tempo real para planejar a viagem',
|
||||
'admin.addons.catalog.collab.description': 'Notas, enquetes e chat em tempo real para planejar a viagem',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description':
|
||||
'Model Context Protocol para integração com assistentes de IA',
|
||||
'admin.addons.subtitleBefore':
|
||||
'Ative ou desative recursos para personalizar sua ',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol para integração com assistentes de IA',
|
||||
'admin.addons.subtitleBefore': 'Ative ou desative recursos para personalizar sua ',
|
||||
'admin.addons.subtitleAfter': ' experiência.',
|
||||
'admin.addons.enabled': 'Ativado',
|
||||
'admin.addons.disabled': 'Desativado',
|
||||
@@ -211,13 +182,11 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.type.global': 'Global',
|
||||
'admin.addons.type.integration': 'Integração',
|
||||
'admin.addons.tripHint': 'Disponível como aba em cada viagem',
|
||||
'admin.addons.globalHint':
|
||||
'Disponível como seção própria na navegação principal',
|
||||
'admin.addons.globalHint': 'Disponível como seção própria na navegação principal',
|
||||
'admin.addons.toast.updated': 'Complemento atualizado',
|
||||
'admin.addons.toast.error': 'Falha ao atualizar complemento',
|
||||
'admin.addons.noAddons': 'Nenhum complemento disponível',
|
||||
'admin.addons.integrationHint':
|
||||
'Serviços de backend e integrações de API sem página dedicada',
|
||||
'admin.addons.integrationHint': 'Serviços de backend e integrações de API sem página dedicada',
|
||||
'admin.weather.title': 'Dados meteorológicos',
|
||||
'admin.weather.badge': 'Desde 24 de março de 2026',
|
||||
'admin.weather.description':
|
||||
@@ -225,15 +194,13 @@ const admin: TranslationStrings = {
|
||||
'admin.weather.forecast': 'Previsão de 16 dias',
|
||||
'admin.weather.forecastDesc': 'Antes eram 5 dias (OpenWeatherMap)',
|
||||
'admin.weather.climate': 'Dados climáticos históricos',
|
||||
'admin.weather.climateDesc':
|
||||
'Médias dos últimos 85 anos para dias além da previsão de 16 dias',
|
||||
'admin.weather.climateDesc': '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.requestsDesc': 'Grátis, sem chave de API',
|
||||
'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.',
|
||||
'admin.tabs.audit': 'Auditoria',
|
||||
'admin.audit.subtitle':
|
||||
'Eventos sensíveis de segurança e administração (backups, usuários, 2FA, configurações).',
|
||||
'admin.audit.subtitle': 'Eventos sensíveis de segurança e administração (backups, usuários, 2FA, configurações).',
|
||||
'admin.audit.empty': 'Nenhum registro de auditoria.',
|
||||
'admin.audit.refresh': 'Atualizar',
|
||||
'admin.audit.loadMore': 'Carregar mais',
|
||||
@@ -257,8 +224,7 @@ const admin: TranslationStrings = {
|
||||
'admin.github.by': 'por',
|
||||
'admin.github.support': 'Ajuda a continuar desenvolvendo o TREK',
|
||||
'admin.update.available': 'Atualização disponível',
|
||||
'admin.update.text':
|
||||
'O TREK {version} está disponível. Você está na {current}.',
|
||||
'admin.update.text': 'O TREK {version} está disponível. Você está na {current}.',
|
||||
'admin.update.button': 'Ver no GitHub',
|
||||
'admin.update.install': '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.',
|
||||
'admin.update.dataInfo':
|
||||
'Todos os seus dados (viagens, usuários, chaves de API, envios, Vacay, Atlas, orçamentos) serão preservados.',
|
||||
'admin.update.warning':
|
||||
'O app ficará brevemente indisponível durante o reinício.',
|
||||
'admin.update.warning': 'O app ficará brevemente indisponível durante o reinício.',
|
||||
'admin.update.confirm': 'Atualizar agora',
|
||||
'admin.update.installing': 'Atualizando…',
|
||||
'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.backupLink': 'Ir para Backup',
|
||||
'admin.update.howTo': 'Como atualizar',
|
||||
'admin.update.dockerText':
|
||||
'Sua instância TREK roda no Docker. Para atualizar para {version}, execute no servidor:',
|
||||
'admin.update.dockerText': '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.tabs.permissions': 'Permissões',
|
||||
'admin.tabs.mcpTokens': 'Acesso MCP',
|
||||
'admin.mcpTokens.title': 'Acesso MCP',
|
||||
'admin.mcpTokens.subtitle':
|
||||
'Gerenciar sessões OAuth e tokens de API de todos os usuários',
|
||||
'admin.mcpTokens.subtitle': 'Gerenciar sessões OAuth e tokens de API de todos os usuários',
|
||||
'admin.mcpTokens.sectionTitle': 'Tokens de API',
|
||||
'admin.mcpTokens.owner': 'Proprietário',
|
||||
'admin.mcpTokens.tokenName': 'Nome do Token',
|
||||
@@ -303,8 +266,7 @@ const admin: TranslationStrings = {
|
||||
'admin.oauthSessions.created': 'Criado',
|
||||
'admin.oauthSessions.empty': 'Nenhuma sessão OAuth ativa',
|
||||
'admin.oauthSessions.revokeTitle': 'Revogar sessão',
|
||||
'admin.oauthSessions.revokeMessage':
|
||||
'Esta sessão OAuth será revogada imediatamente. O cliente perderá o acesso MCP.',
|
||||
'admin.oauthSessions.revokeMessage': 'Esta sessão OAuth será revogada imediatamente. O cliente perderá o acesso MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sessão revogada',
|
||||
'admin.oauthSessions.revokeError': 'Falha ao revogar sessão',
|
||||
'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.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.',
|
||||
'admin.notifications.adminWebhookPanel.saved':
|
||||
'URL do webhook de admin salva',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess':
|
||||
'Webhook de teste enviado com sucesso',
|
||||
'admin.notifications.adminWebhookPanel.testFailed':
|
||||
'Falha no webhook de teste',
|
||||
'admin.notifications.adminWebhookPanel.saved': 'URL do webhook de admin salva',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de teste enviado com sucesso',
|
||||
'admin.notifications.adminWebhookPanel.testFailed': 'Falha no webhook de teste',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint':
|
||||
'O webhook de admin dispara automaticamente quando uma URL está configurada',
|
||||
'admin.notifications.ntfy': 'Ntfy',
|
||||
@@ -340,32 +299,25 @@ const admin: TranslationStrings = {
|
||||
'admin.notifications.adminNtfyPanel.topicLabel': 'Tópico de admin',
|
||||
'admin.notifications.adminNtfyPanel.topicPlaceholder': 'trek-admin-alerts',
|
||||
'admin.notifications.adminNtfyPanel.tokenLabel': 'Token de acesso (opcional)',
|
||||
'admin.notifications.adminNtfyPanel.tokenCleared':
|
||||
'Token de acesso admin removido',
|
||||
'admin.notifications.adminNtfyPanel.saved':
|
||||
'Configurações de Ntfy de admin salvas',
|
||||
'admin.notifications.adminNtfyPanel.tokenCleared': 'Token de acesso admin removido',
|
||||
'admin.notifications.adminNtfyPanel.saved': 'Configurações de Ntfy de admin salvas',
|
||||
'admin.notifications.adminNtfyPanel.test': 'Enviar Ntfy de teste',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess':
|
||||
'Ntfy de teste enviado com sucesso',
|
||||
'admin.notifications.adminNtfyPanel.testFailed':
|
||||
'Falha ao enviar Ntfy de teste',
|
||||
'admin.notifications.adminNtfyPanel.alwaysOnHint':
|
||||
'O Ntfy de admin sempre dispara quando um tópico está configurado',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess': 'Ntfy de teste enviado com sucesso',
|
||||
'admin.notifications.adminNtfyPanel.testFailed': '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':
|
||||
'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.hint':
|
||||
'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.disabled':
|
||||
'Lembretes de viagem desativados',
|
||||
'admin.notifications.tripReminders.disabled': 'Lembretes de viagem desativados',
|
||||
'admin.tabs.notifications': 'Notificações',
|
||||
'admin.addons.catalog.journey.name': 'Jornada',
|
||||
'admin.addons.catalog.journey.description':
|
||||
'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.cardHint':
|
||||
'Permite que os usuários entrem com passkeys (WebAuthn). Desativado por padrão.',
|
||||
'admin.passkey.cardHint': 'Permite que os usuários entrem com passkeys (WebAuthn). Desativado por padrão.',
|
||||
'admin.passkey.login': 'Ativar login com passkey',
|
||||
'admin.passkey.loginHint':
|
||||
'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.resetDone': 'Removida(s) {count} passkey(s)',
|
||||
'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.providerMapbox': 'Mapbox (3D)',
|
||||
'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.mapboxStylePlaceholder': 'Escolha um estilo…',
|
||||
'admin.defaultSettings.mapbox3d': 'Edifícios & relevo em 3D',
|
||||
|
||||
@@ -30,8 +30,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.visitedCountries': 'Países visitados',
|
||||
'atlas.cities': 'Cidades',
|
||||
'atlas.noData': 'Ainda sem dados de viagem',
|
||||
'atlas.noDataHint':
|
||||
'Crie uma viagem e adicione lugares para ver o mapa mundial',
|
||||
'atlas.noDataHint': 'Crie uma viagem e adicione lugares para ver o mapa mundial',
|
||||
'atlas.lastTrip': 'Última viagem',
|
||||
'atlas.nextTrip': 'Próxima viagem',
|
||||
'atlas.daysLeft': 'dias restantes',
|
||||
|
||||
@@ -12,10 +12,8 @@ const backup: TranslationStrings = {
|
||||
'backup.createFirst': 'Criar primeiro backup',
|
||||
'backup.download': 'Baixar',
|
||||
'backup.restore': 'Restaurar',
|
||||
'backup.confirm.restore':
|
||||
'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.restore': '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.delete': 'Excluir o backup "{name}"?',
|
||||
'backup.toast.loadError': 'Falha ao carregar backups',
|
||||
'backup.toast.created': 'Backup criado com sucesso',
|
||||
@@ -31,15 +29,13 @@ const backup: TranslationStrings = {
|
||||
'backup.auto.title': 'Backup automático',
|
||||
'backup.auto.subtitle': 'Backup automático em agenda',
|
||||
'backup.auto.enable': 'Ativar backup automático',
|
||||
'backup.auto.enableHint':
|
||||
'Backups serão criados automaticamente conforme a agenda escolhida',
|
||||
'backup.auto.enableHint': 'Backups serão criados automaticamente conforme a agenda escolhida',
|
||||
'backup.auto.interval': 'Intervalo',
|
||||
'backup.auto.hour': 'Executar no horário',
|
||||
'backup.auto.hourHint': 'Horário local do servidor (formato {format})',
|
||||
'backup.auto.dayOfWeek': 'Dia da semana',
|
||||
'backup.auto.dayOfMonth': 'Dia do mês',
|
||||
'backup.auto.dayOfMonthHint':
|
||||
'Limitado a 1–28 para compatibilidade com todos os meses',
|
||||
'backup.auto.dayOfMonthHint': 'Limitado a 1–28 para compatibilidade com todos os meses',
|
||||
'backup.auto.scheduleSummary': 'Agenda',
|
||||
'backup.auto.summaryDaily': 'Todos os dias às {hour}:00',
|
||||
'backup.auto.summaryWeekly': 'Toda {day} às {hour}:00',
|
||||
@@ -48,8 +44,7 @@ const backup: TranslationStrings = {
|
||||
'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.',
|
||||
'backup.auto.copyEnv': 'Copiar variáveis de ambiente Docker',
|
||||
'backup.auto.envCopied':
|
||||
'Variáveis de ambiente Docker copiadas para a área de transferência',
|
||||
'backup.auto.envCopied': 'Variáveis de ambiente Docker copiadas para a área de transferência',
|
||||
'backup.auto.keepLabel': 'Excluir backups antigos após',
|
||||
'backup.dow.sunday': 'Dom',
|
||||
'backup.dow.monday': 'Seg',
|
||||
@@ -71,8 +66,7 @@ const backup: TranslationStrings = {
|
||||
'backup.restoreConfirmTitle': 'Restaurar backup?',
|
||||
'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.',
|
||||
'backup.restoreTip':
|
||||
'Dica: crie um backup do estado atual antes de restaurar.',
|
||||
'backup.restoreTip': 'Dica: crie um backup do estado atual antes de restaurar.',
|
||||
'backup.restoreConfirm': 'Sim, restaurar',
|
||||
};
|
||||
export default backup;
|
||||
|
||||
@@ -4,8 +4,7 @@ const budget: TranslationStrings = {
|
||||
'budget.title': 'Orçamento',
|
||||
'budget.exportCsv': 'Exportar CSV',
|
||||
'budget.emptyTitle': 'Nenhum orçamento criado ainda',
|
||||
'budget.emptyText':
|
||||
'Crie categorias e lançamentos para planejar o orçamento da viagem',
|
||||
'budget.emptyText': 'Crie categorias e lançamentos para planejar o orçamento da viagem',
|
||||
'budget.emptyPlaceholder': 'Nome da categoria...',
|
||||
'budget.createCategory': 'Criar categoria',
|
||||
'budget.category': 'Categoria',
|
||||
@@ -27,8 +26,7 @@ const budget: TranslationStrings = {
|
||||
'budget.byCategory': 'Por categoria',
|
||||
'budget.editTooltip': 'Clique para editar',
|
||||
'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome por lá',
|
||||
'budget.confirm.deleteCategory':
|
||||
'Excluir a categoria "{name}" com {count} lançamento(s)?',
|
||||
'budget.confirm.deleteCategory': 'Excluir a categoria "{name}" com {count} lançamento(s)?',
|
||||
'budget.deleteCategory': 'Excluir categoria',
|
||||
'budget.perPerson': 'Por pessoa',
|
||||
'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.',
|
||||
'budget.netBalances': 'Saldos líquidos',
|
||||
'budget.categoriesLabel': 'categorias',
|
||||
"costs.you": "Você",
|
||||
"costs.youShort": "V",
|
||||
"costs.youLower": "você",
|
||||
"costs.youOwe": "Você deve",
|
||||
"costs.youOweSub": "Você deve pagar os outros",
|
||||
"costs.youreOwed": "Devem a você",
|
||||
"costs.youreOwedSub": "Os outros devem pagar você",
|
||||
"costs.totalSpend": "Gasto total da viagem",
|
||||
"costs.totalSpendSub": "Entre todos os viajantes",
|
||||
"costs.to": "Para",
|
||||
"costs.from": "De",
|
||||
"costs.allSettled": "Suas contas estão acertadas",
|
||||
"costs.nothingOwed": "Ninguém deve nada a você",
|
||||
"costs.yourShare": "Sua parte",
|
||||
"costs.youPaid": "Você pagou",
|
||||
"costs.expenses": "Despesas",
|
||||
"costs.entries": "{count} lançamentos",
|
||||
"costs.searchPlaceholder": "Buscar despesas…",
|
||||
"costs.filter.all": "Todas",
|
||||
"costs.filter.mine": "Pagas por mim",
|
||||
"costs.filter.owed": "Devem a mim",
|
||||
"costs.addExpense": "Adicionar despesa",
|
||||
"costs.editExpense": "Editar despesa",
|
||||
"costs.noMatch": "Nenhuma despesa corresponde à busca.",
|
||||
"costs.emptyText": "Nenhuma despesa ainda. Adicione a primeira.",
|
||||
"costs.spent": "{amount} gastos",
|
||||
"costs.noDate": "Sem data",
|
||||
"costs.noOnePaid": "Ninguém pagou ainda",
|
||||
"costs.youLent": "você emprestou {amount}",
|
||||
"costs.youBorrowed": "você pegou emprestado {amount}",
|
||||
"costs.settleUp": "Acertar contas",
|
||||
"costs.history": "Histórico",
|
||||
"costs.everyoneSquare": "Todos quitados",
|
||||
"costs.nothingOutstanding": "Nenhum pagamento pendente no momento.",
|
||||
"costs.pay": "paga",
|
||||
"costs.pays": "paga",
|
||||
"costs.settle": "Acertar",
|
||||
"costs.balances": "Saldos",
|
||||
"costs.byCategory": "Por categoria",
|
||||
"costs.noCategories": "Nenhuma despesa ainda.",
|
||||
"costs.settleHistory": "Histórico de acertos",
|
||||
"costs.noSettlements": "Nenhum pagamento acertado ainda.",
|
||||
"costs.paymentsSettled": "{count} pagamentos acertados",
|
||||
"costs.paid": "pago",
|
||||
"costs.undo": "Desfazer",
|
||||
"costs.whatFor": "Para que foi?",
|
||||
"costs.namePlaceholder": "ex.: jantar, lembranças, gasolina…",
|
||||
"costs.totalAmount": "Valor total",
|
||||
"costs.currency": "Moeda",
|
||||
"costs.day": "Dia",
|
||||
"costs.rateLabel": "1 {from} em {to}",
|
||||
"costs.category": "Categoria",
|
||||
"costs.whoPaid": "Quem pagou?",
|
||||
"costs.splitBetween": "Dividir igualmente entre",
|
||||
"costs.pickSomeone": "Escolha pelo menos uma pessoa para dividir.",
|
||||
"costs.splitSummary": "Dividido entre {count} · {amount} cada",
|
||||
"costs.cat.accommodation": "Hospedagem",
|
||||
"costs.cat.food": "Comida e bebida",
|
||||
"costs.cat.groceries": "Mercado",
|
||||
"costs.cat.transport": "Transporte",
|
||||
"costs.cat.flights": "Voos",
|
||||
"costs.cat.activities": "Atividades",
|
||||
"costs.cat.sightseeing": "Passeios turísticos",
|
||||
"costs.cat.shopping": "Compras",
|
||||
"costs.cat.fees": "Taxas e ingressos",
|
||||
"costs.cat.health": "Saúde",
|
||||
"costs.cat.tips": "Gorjetas",
|
||||
"costs.cat.other": "Outros",
|
||||
"costs.daysCount": "{count} dias",
|
||||
"costs.travelers": "{count} viajantes",
|
||||
"costs.liveRate": "taxa ao vivo",
|
||||
"costs.settleAll": "Acertar tudo",
|
||||
'costs.you': 'Você',
|
||||
'costs.youShort': 'V',
|
||||
'costs.youLower': 'você',
|
||||
'costs.youOwe': 'Você deve',
|
||||
'costs.youOweSub': 'Você deve pagar os outros',
|
||||
'costs.youreOwed': 'Devem a você',
|
||||
'costs.youreOwedSub': 'Os outros devem pagar você',
|
||||
'costs.totalSpend': 'Gasto total da viagem',
|
||||
'costs.totalSpendSub': 'Entre todos os viajantes',
|
||||
'costs.to': 'Para',
|
||||
'costs.from': 'De',
|
||||
'costs.allSettled': 'Suas contas estão acertadas',
|
||||
'costs.nothingOwed': 'Ninguém deve nada a você',
|
||||
'costs.yourShare': 'Sua parte',
|
||||
'costs.youPaid': 'Você pagou',
|
||||
'costs.expenses': 'Despesas',
|
||||
'costs.entries': '{count} lançamentos',
|
||||
'costs.searchPlaceholder': 'Buscar despesas…',
|
||||
'costs.filter.all': 'Todas',
|
||||
'costs.filter.mine': 'Pagas por mim',
|
||||
'costs.filter.owed': 'Devem a mim',
|
||||
'costs.addExpense': 'Adicionar despesa',
|
||||
'costs.editExpense': 'Editar despesa',
|
||||
'costs.noMatch': 'Nenhuma despesa corresponde à busca.',
|
||||
'costs.emptyText': 'Nenhuma despesa ainda. Adicione a primeira.',
|
||||
'costs.spent': '{amount} gastos',
|
||||
'costs.noDate': 'Sem data',
|
||||
'costs.noOnePaid': 'Ninguém pagou ainda',
|
||||
'costs.youLent': 'você emprestou {amount}',
|
||||
'costs.youBorrowed': 'você pegou emprestado {amount}',
|
||||
'costs.settleUp': 'Acertar contas',
|
||||
'costs.history': 'Histórico',
|
||||
'costs.everyoneSquare': 'Todos quitados',
|
||||
'costs.nothingOutstanding': 'Nenhum pagamento pendente no momento.',
|
||||
'costs.pay': 'paga',
|
||||
'costs.pays': 'paga',
|
||||
'costs.settle': 'Acertar',
|
||||
'costs.balances': 'Saldos',
|
||||
'costs.byCategory': 'Por categoria',
|
||||
'costs.noCategories': 'Nenhuma despesa ainda.',
|
||||
'costs.settleHistory': 'Histórico de acertos',
|
||||
'costs.noSettlements': 'Nenhum pagamento acertado ainda.',
|
||||
'costs.paymentsSettled': '{count} pagamentos acertados',
|
||||
'costs.paid': 'pago',
|
||||
'costs.undo': 'Desfazer',
|
||||
'costs.whatFor': 'Para que foi?',
|
||||
'costs.namePlaceholder': 'ex.: jantar, lembranças, gasolina…',
|
||||
'costs.totalAmount': 'Valor total',
|
||||
'costs.currency': 'Moeda',
|
||||
'costs.day': 'Dia',
|
||||
'costs.rateLabel': '1 {from} em {to}',
|
||||
'costs.category': 'Categoria',
|
||||
'costs.whoPaid': 'Quem pagou?',
|
||||
'costs.splitBetween': 'Dividir igualmente entre',
|
||||
'costs.pickSomeone': 'Escolha pelo menos uma pessoa para dividir.',
|
||||
'costs.splitSummary': 'Dividido entre {count} · {amount} cada',
|
||||
'costs.cat.accommodation': 'Hospedagem',
|
||||
'costs.cat.food': 'Comida e bebida',
|
||||
'costs.cat.groceries': 'Mercado',
|
||||
'costs.cat.transport': 'Transporte',
|
||||
'costs.cat.flights': 'Voos',
|
||||
'costs.cat.activities': 'Atividades',
|
||||
'costs.cat.sightseeing': 'Passeios turísticos',
|
||||
'costs.cat.shopping': 'Compras',
|
||||
'costs.cat.fees': 'Taxas e ingressos',
|
||||
'costs.cat.health': 'Saúde',
|
||||
'costs.cat.tips': 'Gorjetas',
|
||||
'costs.cat.other': 'Outros',
|
||||
'costs.daysCount': '{count} dias',
|
||||
'costs.travelers': '{count} viajantes',
|
||||
'costs.liveRate': 'taxa ao vivo',
|
||||
'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;
|
||||
|
||||
@@ -13,8 +13,7 @@ const categories: TranslationStrings = {
|
||||
'categories.defaultName': 'Categoria',
|
||||
'categories.update': 'Atualizar',
|
||||
'categories.create': 'Criar',
|
||||
'categories.confirm.delete':
|
||||
'Excluir categoria? Os lugares desta categoria não serão excluídos.',
|
||||
'categories.confirm.delete': 'Excluir categoria? Os lugares desta categoria não serão excluídos.',
|
||||
'categories.toast.loadError': 'Falha ao carregar categorias',
|
||||
'categories.toast.nameRequired': 'Digite um nome',
|
||||
'categories.toast.updated': 'Categoria atualizada',
|
||||
|
||||
@@ -13,10 +13,8 @@ const collab: TranslationStrings = {
|
||||
'collab.chat.send': 'Enviar',
|
||||
'collab.chat.placeholder': 'Digite uma mensagem...',
|
||||
'collab.chat.empty': 'Inicie a conversa',
|
||||
'collab.chat.emptyHint':
|
||||
'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.emptyHint': '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.today': 'Hoje',
|
||||
'collab.chat.yesterday': 'Ontem',
|
||||
'collab.chat.deletedMessage': 'apagou uma mensagem',
|
||||
|
||||
@@ -20,8 +20,7 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.timezoneCustomTzPlaceholder': 'ex.: America/Sao_Paulo',
|
||||
'dashboard.timezoneCustomAdd': 'Adicionar',
|
||||
'dashboard.timezoneCustomErrorEmpty': 'Informe um identificador de fuso',
|
||||
'dashboard.timezoneCustomErrorInvalid':
|
||||
'Fuso inválido. Use o formato Europe/Berlin',
|
||||
'dashboard.timezoneCustomErrorInvalid': 'Fuso inválido. Use o formato Europe/Berlin',
|
||||
'dashboard.timezoneCustomErrorDuplicate': 'Já adicionado',
|
||||
'dashboard.emptyTitle': 'Nenhuma viagem ainda',
|
||||
'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.copied': 'Viagem copiada!',
|
||||
'dashboard.toast.copyError': 'Não foi possível copiar a viagem',
|
||||
'dashboard.confirm.delete':
|
||||
'Excluir a viagem "{title}"? Todos os lugares e planos serão excluídos permanentemente.',
|
||||
'dashboard.confirm.delete': 'Excluir a viagem "{title}"? Todos os lugares e planos serão excluídos permanentemente.',
|
||||
'dashboard.editTrip': 'Editar viagem',
|
||||
'dashboard.createTrip': 'Criar nova viagem',
|
||||
'dashboard.tripTitle': 'Título',
|
||||
@@ -66,10 +64,8 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.startDate': 'Data de início',
|
||||
'dashboard.endDate': 'Data de término',
|
||||
'dashboard.dayCount': 'Número de dias',
|
||||
'dashboard.dayCountHint':
|
||||
'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.dayCountHint': '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.coverImage': 'Imagem de capa',
|
||||
'dashboard.addCoverImage': 'Adicionar capa (ou arrastar e soltar)',
|
||||
'dashboard.addMembers': 'Companheiros de viagem',
|
||||
|
||||
@@ -7,10 +7,8 @@ const day: TranslationStrings = {
|
||||
'day.sunrise': 'Nascer do sol',
|
||||
'day.sunset': 'Pôr do sol',
|
||||
'day.hourlyForecast': 'Previsão por hora',
|
||||
'day.climateHint':
|
||||
'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.climateHint': '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.overview': 'Resumo do dia',
|
||||
'day.accommodation': 'Hospedagem',
|
||||
'day.addAccommodation': 'Adicionar hospedagem',
|
||||
|
||||
@@ -17,30 +17,24 @@ const dayplan: TranslationStrings = {
|
||||
'dayplan.optimize': 'Otimizar',
|
||||
'dayplan.optimized': 'Rota otimizada',
|
||||
'dayplan.routeError': 'Falha ao calcular a rota',
|
||||
'dayplan.toast.needTwoPlaces':
|
||||
'São necessários pelo menos dois lugares para otimizar a rota',
|
||||
'dayplan.toast.needTwoPlaces': 'São necessários pelo menos dois lugares para otimizar a rota',
|
||||
'dayplan.toast.routeOptimized': 'Rota otimizada',
|
||||
'dayplan.toast.routeOptimizedFromHotel':
|
||||
'Rota otimizada a partir da sua hospedagem',
|
||||
'dayplan.toast.noGeoPlaces':
|
||||
'Nenhum lugar com coordenadas para calcular a rota',
|
||||
'dayplan.toast.routeOptimizedFromHotel': 'Rota otimizada a partir da sua hospedagem',
|
||||
'dayplan.toast.noGeoPlaces': 'Nenhum lugar com coordenadas para calcular a rota',
|
||||
'dayplan.confirmed': 'Confirmada',
|
||||
'dayplan.pendingRes': 'Pendente',
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'Exportar plano do dia em PDF',
|
||||
'dayplan.pdfError': 'Falha ao exportar PDF',
|
||||
'dayplan.cannotReorderTransport':
|
||||
'Reservas com horário fixo não podem ser reordenadas',
|
||||
'dayplan.cannotReorderTransport': 'Reservas com horário fixo não podem ser reordenadas',
|
||||
'dayplan.confirmRemoveTimeTitle': 'Remover horário?',
|
||||
'dayplan.confirmRemoveTimeBody':
|
||||
'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.confirmDeleteNoteTitle': 'Excluir nota?',
|
||||
'dayplan.confirmDeleteNoteBody': 'Esta nota será excluída permanentemente.',
|
||||
'dayplan.cannotDropOnTimed':
|
||||
'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.cannotDropOnTimed': '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.mobile.addPlace': 'Adicionar lugar',
|
||||
'dayplan.mobile.searchPlaces': 'Buscar lugares...',
|
||||
'dayplan.mobile.allAssigned': 'Todos os lugares atribuídos',
|
||||
|
||||
@@ -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.',
|
||||
ctaIntro: 'Redefinir senha',
|
||||
expiry: 'Este link expira em 60 minutos.',
|
||||
ignore:
|
||||
'Se você não solicitou isto, pode ignorar este e-mail — sua senha não será alterada.',
|
||||
ignore: 'Se você não solicitou isto, pode ignorar este e-mail — sua senha não será alterada.',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ const files: TranslationStrings = {
|
||||
'files.uploadError': 'Falha no envio',
|
||||
'files.dropzone': 'Solte os arquivos aqui',
|
||||
'files.dropzoneHint': 'ou clique para escolher',
|
||||
'files.allowedTypes':
|
||||
'Imagens, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB',
|
||||
'files.allowedTypes': 'Imagens, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB',
|
||||
'files.uploading': 'Enviando...',
|
||||
'files.filterAll': 'Todos',
|
||||
'files.filterPdf': 'PDFs',
|
||||
@@ -32,8 +31,7 @@ const files: TranslationStrings = {
|
||||
'files.sourceBooking': 'Reserva',
|
||||
'files.sourceTransport': 'Transporte',
|
||||
'files.attach': 'Anexar',
|
||||
'files.pasteHint':
|
||||
'Você também pode colar imagens da área de transferência (Ctrl+V)',
|
||||
'files.pasteHint': 'Você também pode colar imagens da área de transferência (Ctrl+V)',
|
||||
'files.trash': 'Lixeira',
|
||||
'files.trashEmpty': 'A lixeira está vazia',
|
||||
'files.emptyTrash': 'Esvaziar lixeira',
|
||||
@@ -53,10 +51,8 @@ const files: TranslationStrings = {
|
||||
'files.toast.assigned': 'Arquivo atribuído',
|
||||
'files.toast.assignError': 'Falha na atribuição',
|
||||
'files.toast.restoreError': 'Falha ao restaurar',
|
||||
'files.confirm.permanentDelete':
|
||||
'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.permanentDelete': '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.noteLabel': 'Nota',
|
||||
'files.notePlaceholder': 'Adicione uma nota...',
|
||||
};
|
||||
|
||||
@@ -14,14 +14,12 @@ const journey: TranslationStrings = {
|
||||
'journey.createError': 'Não foi possível criar a jornada',
|
||||
'journey.deleteError': 'Não foi possível excluir a jornada',
|
||||
'journey.deleteConfirmTitle': 'Excluir',
|
||||
'journey.deleteConfirmMessage':
|
||||
'Excluir "{title}"? Isso não pode ser desfeito.',
|
||||
'journey.deleteConfirmMessage': 'Excluir "{title}"? Isso não pode ser desfeito.',
|
||||
'journey.deleteConfirmGeneric': 'Tem certeza de que deseja excluir isso?',
|
||||
'journey.notFound': 'Jornada não encontrada',
|
||||
'journey.photos': 'Fotos',
|
||||
'journey.timelineEmpty': 'Nenhuma parada ainda',
|
||||
'journey.timelineEmptyHint':
|
||||
'Adicione um check-in ou escreva uma entrada no diário para começar',
|
||||
'journey.timelineEmptyHint': 'Adicione um check-in ou escreva uma entrada no diário para começar',
|
||||
'journey.status.draft': 'Rascunho',
|
||||
'journey.status.active': 'Ativa',
|
||||
'journey.status.completed': 'Concluída',
|
||||
@@ -47,30 +45,25 @@ const journey: TranslationStrings = {
|
||||
'journey.editor.titlePlaceholder': 'Dê um nome a este momento...',
|
||||
'journey.editor.bodyPlaceholder': 'Conte a história deste dia...',
|
||||
'journey.editor.placePlaceholder': 'Localização (opcional)',
|
||||
'journey.editor.tagsPlaceholder':
|
||||
'Tags: joia escondida, melhor refeição, preciso voltar...',
|
||||
'journey.editor.tagsPlaceholder': 'Tags: joia escondida, melhor refeição, preciso voltar...',
|
||||
'journey.visibility.private': 'Privado',
|
||||
'journey.visibility.shared': 'Compartilhado',
|
||||
'journey.visibility.public': 'Público',
|
||||
'journey.emptyState.title': 'Sua história começa aqui',
|
||||
'journey.emptyState.subtitle':
|
||||
'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.emptyState.subtitle': '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.createJourney': 'Criar jornada',
|
||||
'journey.frontpage.activeJourney': 'Jornada ativa',
|
||||
'journey.frontpage.allJourneys': 'Todas as jornadas',
|
||||
'journey.frontpage.journeys': 'jornadas',
|
||||
'journey.frontpage.createNew': 'Criar uma nova jornada',
|
||||
'journey.frontpage.createNewSub':
|
||||
'Escolha viagens, escreva histórias, compartilhe suas aventuras',
|
||||
'journey.frontpage.createNewSub': 'Escolha viagens, escreva histórias, compartilhe suas aventuras',
|
||||
'journey.frontpage.live': 'Ao vivo',
|
||||
'journey.frontpage.synced': 'Sincronizado',
|
||||
'journey.frontpage.continueWriting': 'Continuar escrevendo',
|
||||
'journey.frontpage.updated': 'Atualizado {time}',
|
||||
'journey.frontpage.suggestionLabel': 'A viagem acabou de terminar',
|
||||
'journey.frontpage.suggestionText':
|
||||
'Transforme <strong>{title}</strong> em uma jornada',
|
||||
'journey.frontpage.suggestionText': 'Transforme <strong>{title}</strong> em uma jornada',
|
||||
'journey.frontpage.dismiss': 'Dispensar',
|
||||
'journey.frontpage.journeyName': 'Nome da jornada',
|
||||
'journey.frontpage.namePlaceholder': 'ex. Sudeste Asiático 2026',
|
||||
@@ -85,11 +78,9 @@ const journey: TranslationStrings = {
|
||||
'journey.detail.newEntry': 'Nova entrada',
|
||||
'journey.detail.editEntry': 'Editar entrada',
|
||||
'journey.detail.noEntries': 'Nenhuma entrada ainda',
|
||||
'journey.detail.noEntriesHint':
|
||||
'Adicione uma viagem para começar com entradas preliminares',
|
||||
'journey.detail.noEntriesHint': 'Adicione uma viagem para começar com entradas preliminares',
|
||||
'journey.detail.noPhotos': 'Nenhuma foto ainda',
|
||||
'journey.detail.noPhotosHint':
|
||||
'Envie fotos para as entradas ou explore sua biblioteca do Immich/Synology',
|
||||
'journey.detail.noPhotosHint': 'Envie fotos para as entradas ou explore sua biblioteca do Immich/Synology',
|
||||
'journey.detail.journeyStats': 'Estatísticas da jornada',
|
||||
'journey.detail.syncedTrips': 'Viagens sincronizadas',
|
||||
'journey.detail.noTripsLinked': 'Nenhuma viagem vinculada ainda',
|
||||
@@ -110,14 +101,12 @@ const journey: TranslationStrings = {
|
||||
'journey.verdict.couldBeBetter': 'Poderia ser melhor',
|
||||
'journey.synced.places': 'lugares',
|
||||
'journey.synced.synced': 'sincronizado',
|
||||
'journey.editor.discardChangesConfirm':
|
||||
'Você tem alterações não salvas. Descartá-las?',
|
||||
'journey.editor.discardChangesConfirm': 'Você tem alterações não salvas. Descartá-las?',
|
||||
'journey.editor.uploadFailed': 'Falha ao enviar fotos',
|
||||
'journey.editor.uploadPhotos': 'Enviar fotos',
|
||||
'journey.editor.uploading': 'Enviando...',
|
||||
'journey.editor.uploadingProgress': 'Enviando {done}/{total}…',
|
||||
'journey.editor.uploadPartialFailed':
|
||||
'{failed} de {total} fotos falharam — salve novamente para tentar',
|
||||
'journey.editor.uploadPartialFailed': '{failed} de {total} fotos falharam — salve novamente para tentar',
|
||||
'journey.editor.fromGallery': 'Da galeria',
|
||||
'journey.editor.allPhotosAdded': 'Todas as fotos já foram adicionadas',
|
||||
'journey.editor.writeStory': 'Escreva sua história...',
|
||||
@@ -196,12 +185,10 @@ const journey: TranslationStrings = {
|
||||
'journey.settings.reopenJourney': 'Restaurar Jornada',
|
||||
'journey.settings.archived': 'Jornada arquivada',
|
||||
'journey.settings.reopened': 'Jornada reaberta',
|
||||
'journey.settings.endDescription':
|
||||
'Oculta o selo Ao Vivo. Você pode reabrir a qualquer momento.',
|
||||
'journey.settings.endDescription': 'Oculta o selo Ao Vivo. Você pode reabrir a qualquer momento.',
|
||||
'journey.settings.delete': 'Excluir',
|
||||
'journey.settings.deleteJourney': 'Excluir jornada',
|
||||
'journey.settings.deleteMessage':
|
||||
'Excluir "{title}"? Todas as entradas e fotos serão perdidas.',
|
||||
'journey.settings.deleteMessage': 'Excluir "{title}"? Todas as entradas e fotos serão perdidas.',
|
||||
'journey.settings.saved': 'Configurações salvas',
|
||||
'journey.settings.saveFailed': 'Não foi possível salvar',
|
||||
'journey.settings.coverUpdated': 'Capa atualizada',
|
||||
@@ -212,8 +199,7 @@ const journey: TranslationStrings = {
|
||||
'journey.photosUploadFailed': 'Algumas fotos não foram enviadas',
|
||||
'journey.photosAdded': '{count} fotos adicionadas',
|
||||
'journey.public.notFound': 'Não encontrado',
|
||||
'journey.public.notFoundMessage':
|
||||
'Esta jornada não existe ou o link expirou.',
|
||||
'journey.public.notFoundMessage': 'Esta jornada não existe ou o link expirou.',
|
||||
'journey.public.readOnly': 'Somente leitura · Jornada pública',
|
||||
'journey.public.tagline': 'Kit de recursos e exploração de viagens',
|
||||
'journey.public.sharedVia': 'Compartilhado via',
|
||||
|
||||
+11
-22
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const login: TranslationStrings = {
|
||||
'login.error': 'Falha no login. Verifique suas credenciais.',
|
||||
'login.tagline': 'Suas viagens.\nSeu plano.',
|
||||
'login.description':
|
||||
'Planeje viagens em equipe com mapas interativos, orçamento e sincronização em tempo real.',
|
||||
'login.description': 'Planeje viagens em equipe com mapas interativos, orçamento e sincronização em tempo real.',
|
||||
'login.features.maps': 'Mapas interativos',
|
||||
'login.features.mapsDesc': 'Google Places, rotas e agrupamento',
|
||||
'login.features.realtime': 'Sincronização em tempo real',
|
||||
@@ -27,8 +26,7 @@ const login: TranslationStrings = {
|
||||
'login.signingIn': 'Entrando…',
|
||||
'login.signIn': 'Entrar',
|
||||
'login.createAdmin': 'Criar conta de administrador',
|
||||
'login.createAdminHint':
|
||||
'Configure a primeira conta de administrador do TREK.',
|
||||
'login.createAdminHint': 'Configure a primeira conta de administrador do TREK.',
|
||||
'login.setNewPassword': 'Definir nova senha',
|
||||
'login.setNewPasswordHint': 'Você deve alterar sua senha antes de continuar.',
|
||||
'login.createAccount': 'Criar conta',
|
||||
@@ -39,16 +37,14 @@ const login: TranslationStrings = {
|
||||
'login.register': 'Cadastrar',
|
||||
'login.emailPlaceholder': 'seu@email.com',
|
||||
'login.username': 'Nome de usuário',
|
||||
'login.oidc.registrationDisabled':
|
||||
'Cadastro desativado. Fale com o administrador.',
|
||||
'login.oidc.registrationDisabled': 'Cadastro desativado. Fale com o administrador.',
|
||||
'login.oidc.noEmail': 'Nenhum e-mail recebido do provedor.',
|
||||
'login.oidc.tokenFailed': 'Falha na autenticação.',
|
||||
'login.oidc.invalidState': 'Sessão inválida. Tente novamente.',
|
||||
'login.demoFailed': 'Falha no login de demonstração',
|
||||
'login.oidcSignIn': 'Entrar com {name}',
|
||||
'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.',
|
||||
'login.oidcLoggedOut':
|
||||
'Você foi desconectado. Entre novamente usando o provedor SSO.',
|
||||
'login.oidcLoggedOut': 'Você foi desconectado. Entre novamente usando o provedor SSO.',
|
||||
'login.demoHint': 'Experimente a demonstração — sem cadastro',
|
||||
'login.mfaTitle': 'Autenticação em duas etapas',
|
||||
'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.rememberMe': 'Lembrar de mim',
|
||||
'login.forgotPasswordTitle': 'Redefinir sua senha',
|
||||
'login.forgotPasswordBody':
|
||||
'Digite o e-mail cadastrado. Se houver uma conta, enviaremos um link de redefinição.',
|
||||
'login.forgotPasswordBody': 'Digite o e-mail cadastrado. Se houver uma conta, enviaremos um link de redefinição.',
|
||||
'login.forgotPasswordSubmit': 'Enviar link',
|
||||
'login.forgotPasswordSentTitle': 'Verifique seu e-mail',
|
||||
'login.forgotPasswordSentBody':
|
||||
@@ -78,22 +73,16 @@ const login: TranslationStrings = {
|
||||
'login.passwordsDontMatch': 'As senhas não coincidem',
|
||||
'login.mfaCode': 'Código 2FA',
|
||||
'login.resetPasswordTitle': 'Definir uma nova senha',
|
||||
'login.resetPasswordBody':
|
||||
'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.resetPasswordBody': '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.resetPasswordSubmit': 'Redefinir senha',
|
||||
'login.resetPasswordVerify': 'Verificar e redefinir',
|
||||
'login.resetPasswordSuccessTitle': 'Senha atualizada',
|
||||
'login.resetPasswordSuccessBody':
|
||||
'Agora você pode entrar com sua nova senha.',
|
||||
'login.resetPasswordSuccessBody': 'Agora você pode entrar com sua nova senha.',
|
||||
'login.resetPasswordInvalidLink': 'Link de redefinição inválido',
|
||||
'login.resetPasswordInvalidLinkBody':
|
||||
'Este link está ausente ou corrompido. Solicite um novo para continuar.',
|
||||
'login.resetPasswordFailed':
|
||||
'Falha na redefinição. O link pode ter expirado.',
|
||||
'login.resetPasswordInvalidLinkBody': 'Este link está ausente ou corrompido. Solicite um novo para continuar.',
|
||||
'login.resetPasswordFailed': 'Falha na redefinição. O link pode ter expirado.',
|
||||
'login.passkey.signIn': 'Entrar com uma passkey',
|
||||
'login.passkey.failed':
|
||||
'Falha ao entrar com passkey. Tente novamente.',
|
||||
'login.passkey.failed': 'Falha ao entrar com passkey. Tente novamente.',
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -3,21 +3,18 @@ import type { TranslationStrings } from '../types';
|
||||
const memories: TranslationStrings = {
|
||||
'memories.title': 'Fotos',
|
||||
'memories.notConnected': 'Immich não conectado',
|
||||
'memories.notConnectedHint':
|
||||
'Conecte sua instância Immich nas Configurações para ver suas fotos de viagem aqui.',
|
||||
'memories.notConnectedHint': 'Conecte sua instância Immich nas Configurações para ver suas fotos de viagem aqui.',
|
||||
'memories.notConnectedMultipleHint':
|
||||
'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.noPhotos': 'Nenhuma foto encontrada',
|
||||
'memories.noPhotosHint':
|
||||
'Nenhuma foto encontrada no Immich para o período desta viagem.',
|
||||
'memories.noPhotosHint': 'Nenhuma foto encontrada no Immich para o período desta viagem.',
|
||||
'memories.photosFound': 'fotos',
|
||||
'memories.fromOthers': 'de outros',
|
||||
'memories.sharePhotos': 'Compartilhar fotos',
|
||||
'memories.sharing': 'Compartilhando',
|
||||
'memories.reviewTitle': 'Revise suas fotos',
|
||||
'memories.reviewHint':
|
||||
'Clique nas fotos para excluí-las do compartilhamento.',
|
||||
'memories.reviewHint': 'Clique nas fotos para excluí-las do compartilhamento.',
|
||||
'memories.shareCount': 'Compartilhar {count} fotos',
|
||||
'memories.providerUrl': 'URL do servidor',
|
||||
'memories.providerApiKey': 'Chave de API',
|
||||
@@ -26,8 +23,7 @@ const memories: TranslationStrings = {
|
||||
'memories.providerOTP': 'Código MFA (se habilitado)',
|
||||
'memories.skipSSLVerification': 'Pular verificação de certificado SSL',
|
||||
'memories.immichAutoUpload': 'Espelhar fotos da jornada no Immich ao enviar',
|
||||
'memories.providerUrlHintSynology':
|
||||
'Inclua o caminho do aplicativo Photos na URL, ex. https://nas:5001/photo',
|
||||
'memories.providerUrlHintSynology': 'Inclua o caminho do aplicativo Photos na URL, ex. https://nas:5001/photo',
|
||||
'memories.testConnection': 'Testar conexão',
|
||||
'memories.testShort': 'Testar',
|
||||
'memories.testFirst': 'Teste a conexão primeiro',
|
||||
@@ -38,8 +34,7 @@ const memories: TranslationStrings = {
|
||||
'memories.saved': 'Configurações do {provider_name} salvas',
|
||||
'memories.providerDisconnectedBanner':
|
||||
'Sua conexão com {provider_name} foi perdida. Reconecte nas Configurações para ver as fotos.',
|
||||
'memories.saveError':
|
||||
'Não foi possível salvar as configurações de {provider_name}',
|
||||
'memories.saveError': 'Não foi possível salvar as configurações de {provider_name}',
|
||||
'memories.addPhotos': 'Adicionar fotos',
|
||||
'memories.linkAlbum': 'Vincular álbum',
|
||||
'memories.selectAlbum': 'Selecionar álbum do Immich',
|
||||
@@ -73,11 +68,8 @@ const memories: TranslationStrings = {
|
||||
'memories.error.addPhotos': 'Falha ao adicionar fotos',
|
||||
'memories.error.removePhoto': 'Falha ao remover foto',
|
||||
'memories.error.toggleSharing': 'Falha ao atualizar compartilhamento',
|
||||
'memories.saveRouteNotConfigured':
|
||||
'A rota de salvamento não está configurada para este provedor',
|
||||
'memories.testRouteNotConfigured':
|
||||
'A rota de teste não está configurada para este provedor',
|
||||
'memories.fillRequiredFields':
|
||||
'Por favor preencha todos os campos obrigatórios',
|
||||
'memories.saveRouteNotConfigured': 'A rota de salvamento não está configurada para este provedor',
|
||||
'memories.testRouteNotConfigured': 'A rota de teste não está configurada para este provedor',
|
||||
'memories.fillRequiredFields': 'Por favor preencha todos os campos obrigatórios',
|
||||
};
|
||||
export default memories;
|
||||
|
||||
@@ -14,8 +14,7 @@ const notif: TranslationStrings = {
|
||||
'notif.todo_due.title': 'Tarefa com vencimento',
|
||||
'notif.todo_due.text': '{todo} em {trip} vence em {due}',
|
||||
'notif.vacay_invite.title': 'Convite Vacay Fusion',
|
||||
'notif.vacay_invite.text':
|
||||
'{actor} convidou você para fundir planos de férias',
|
||||
'notif.vacay_invite.text': '{actor} convidou você para fundir planos de férias',
|
||||
'notif.photos_shared.title': 'Fotos compartilhadas',
|
||||
'notif.photos_shared.text': '{actor} compartilhou {count} foto(s) em {trip}',
|
||||
'notif.collab_message.title': 'Nova mensagem',
|
||||
@@ -36,7 +35,6 @@ const notif: TranslationStrings = {
|
||||
'notif.generic.title': 'Notificação',
|
||||
'notif.generic.text': 'Você tem uma nova notificação',
|
||||
'notif.dev.unknown_event.title': '[DEV] Evento desconhecido',
|
||||
'notif.dev.unknown_event.text':
|
||||
'O tipo de evento "{event}" não está registrado em EVENT_NOTIFICATION_CONFIG',
|
||||
'notif.dev.unknown_event.text': 'O tipo de evento "{event}" não está registrado em EVENT_NOTIFICATION_CONFIG',
|
||||
};
|
||||
export default notif;
|
||||
|
||||
@@ -26,8 +26,7 @@ const notifications: TranslationStrings = {
|
||||
'notifications.test.navigateText': 'Notificação de teste de navegação.',
|
||||
'notifications.test.goThere': 'Ir lá',
|
||||
'notifications.test.adminTitle': 'Transmissão do admin',
|
||||
'notifications.test.adminText':
|
||||
'{actor} enviou uma notificação de teste para todos os admins.',
|
||||
'notifications.test.adminText': '{actor} enviou uma notificação de teste para todos os admins.',
|
||||
'notifications.test.tripTitle': '{actor} postou na sua viagem',
|
||||
'notifications.test.tripText': 'Notificação de teste para a viagem "{trip}".',
|
||||
'notifications.versionAvailable.title': 'Atualização disponível',
|
||||
|
||||
+30
-60
@@ -17,80 +17,55 @@ const oauth: TranslationStrings = {
|
||||
'oauth.scope.trips:read.label': 'Ver viagens e itinerários',
|
||||
'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.description':
|
||||
'Criar e atualizar viagens, dias, notas e gerenciar membros',
|
||||
'oauth.scope.trips:write.description': 'Criar e atualizar viagens, dias, notas e gerenciar membros',
|
||||
'oauth.scope.trips:delete.label': 'Excluir viagens',
|
||||
'oauth.scope.trips:delete.description':
|
||||
'Excluir viagens permanentemente — esta ação é irreversível',
|
||||
'oauth.scope.trips:delete.description': 'Excluir viagens permanentemente — esta ação é irreversível',
|
||||
'oauth.scope.trips:share.label': 'Gerenciar links de compartilhamento',
|
||||
'oauth.scope.trips:share.description':
|
||||
'Criar, atualizar e revogar links de compartilhamento públicos',
|
||||
'oauth.scope.trips:share.description': 'Criar, atualizar e revogar links de compartilhamento públicos',
|
||||
'oauth.scope.places:read.label': 'Ver locais e dados do mapa',
|
||||
'oauth.scope.places:read.description':
|
||||
'Ler locais, atribuições de dias, tags e categorias',
|
||||
'oauth.scope.places:read.description': 'Ler locais, atribuições de dias, tags e categorias',
|
||||
'oauth.scope.places:write.label': 'Gerenciar locais',
|
||||
'oauth.scope.places:write.description':
|
||||
'Criar, atualizar e excluir locais, atribuições e tags',
|
||||
'oauth.scope.places:write.description': 'Criar, atualizar e excluir locais, atribuições e tags',
|
||||
'oauth.scope.atlas:read.label': 'Ver Atlas',
|
||||
'oauth.scope.atlas:read.description':
|
||||
'Ler países visitados, regiões e lista de desejos',
|
||||
'oauth.scope.atlas:read.description': 'Ler países visitados, regiões e lista de desejos',
|
||||
'oauth.scope.atlas:write.label': 'Gerenciar Atlas',
|
||||
'oauth.scope.atlas:write.description':
|
||||
'Marcar países e regiões como visitados, gerenciar lista de desejos',
|
||||
'oauth.scope.atlas:write.description': '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.description':
|
||||
'Ler itens, malas e responsáveis por categoria',
|
||||
'oauth.scope.packing:read.description': 'Ler itens, malas e responsáveis por categoria',
|
||||
'oauth.scope.packing:write.label': 'Gerenciar listas de bagagem',
|
||||
'oauth.scope.packing:write.description':
|
||||
'Adicionar, atualizar, excluir, marcar e reordenar itens e malas',
|
||||
'oauth.scope.packing:write.description': 'Adicionar, atualizar, excluir, marcar e reordenar itens e malas',
|
||||
'oauth.scope.todos:read.label': 'Ver listas de tarefas',
|
||||
'oauth.scope.todos:read.description':
|
||||
'Ler tarefas da viagem e responsáveis por categoria',
|
||||
'oauth.scope.todos:read.description': 'Ler tarefas da viagem e responsáveis por categoria',
|
||||
'oauth.scope.todos:write.label': 'Gerenciar listas de tarefas',
|
||||
'oauth.scope.todos:write.description':
|
||||
'Criar, atualizar, marcar, excluir e reordenar tarefas',
|
||||
'oauth.scope.todos:write.description': 'Criar, atualizar, marcar, excluir e reordenar tarefas',
|
||||
'oauth.scope.budget:read.label': 'Ver orçamento',
|
||||
'oauth.scope.budget:read.description':
|
||||
'Ler itens de orçamento e detalhamento de despesas',
|
||||
'oauth.scope.budget:read.description': 'Ler itens de orçamento e detalhamento de despesas',
|
||||
'oauth.scope.budget:write.label': 'Gerenciar orçamento',
|
||||
'oauth.scope.budget:write.description':
|
||||
'Criar, atualizar e excluir itens de orçamento',
|
||||
'oauth.scope.budget:write.description': 'Criar, atualizar e excluir itens de orçamento',
|
||||
'oauth.scope.reservations:read.label': 'Ver reservas',
|
||||
'oauth.scope.reservations:read.description':
|
||||
'Ler reservas e detalhes de acomodação',
|
||||
'oauth.scope.reservations:read.description': 'Ler reservas e detalhes de acomodação',
|
||||
'oauth.scope.reservations:write.label': 'Gerenciar reservas',
|
||||
'oauth.scope.reservations:write.description':
|
||||
'Criar, atualizar, excluir e reordenar reservas',
|
||||
'oauth.scope.reservations:write.description': 'Criar, atualizar, excluir e reordenar reservas',
|
||||
'oauth.scope.collab:read.label': 'Ver colaboração',
|
||||
'oauth.scope.collab:read.description':
|
||||
'Ler notas colaborativas, enquetes e mensagens',
|
||||
'oauth.scope.collab:read.description': 'Ler notas colaborativas, enquetes e mensagens',
|
||||
'oauth.scope.collab:write.label': 'Gerenciar colaboração',
|
||||
'oauth.scope.collab:write.description':
|
||||
'Criar, atualizar e excluir notas, enquetes e mensagens',
|
||||
'oauth.scope.collab:write.description': 'Criar, atualizar e excluir notas, enquetes e mensagens',
|
||||
'oauth.scope.notifications:read.label': 'Ver notificações',
|
||||
'oauth.scope.notifications:read.description':
|
||||
'Ler notificações e contagens não lidas',
|
||||
'oauth.scope.notifications:read.description': 'Ler notificações e contagens não lidas',
|
||||
'oauth.scope.notifications:write.label': 'Gerenciar notificações',
|
||||
'oauth.scope.notifications:write.description':
|
||||
'Marcar notificações como lidas e respondê-las',
|
||||
'oauth.scope.notifications:write.description': 'Marcar notificações como lidas e respondê-las',
|
||||
'oauth.scope.vacay:read.label': 'Ver planos de férias',
|
||||
'oauth.scope.vacay:read.description':
|
||||
'Ler dados de planejamento de férias, entradas e estatísticas',
|
||||
'oauth.scope.vacay:read.description': '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.description':
|
||||
'Criar e gerenciar entradas de férias, feriados e planos de equipe',
|
||||
'oauth.scope.vacay:write.description': '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.description':
|
||||
'Pesquisar locais, resolver URLs de mapa e geocodificar coordenadas',
|
||||
'oauth.scope.geo:read.description': 'Pesquisar locais, resolver URLs de mapa e geocodificar coordenadas',
|
||||
'oauth.scope.weather:read.label': 'Previsão do tempo',
|
||||
'oauth.scope.weather:read.description':
|
||||
'Obter previsão do tempo para locais e datas da viagem',
|
||||
'oauth.scope.weather:read.description': 'Obter previsão do tempo para locais e datas da viagem',
|
||||
'oauth.scope.journey:read.label': 'Ver jornadas',
|
||||
'oauth.scope.journey:read.description':
|
||||
'Ler jornadas, entradas e lista de colaboradores',
|
||||
'oauth.scope.journey:read.description': 'Ler jornadas, entradas e lista de colaboradores',
|
||||
'oauth.scope.journey:write.label': 'Gerenciar jornadas',
|
||||
'oauth.scope.journey:write.description':
|
||||
'Criar, atualizar e excluir jornadas e suas entradas',
|
||||
'oauth.scope.journey:write.description': 'Criar, atualizar e excluir jornadas e suas entradas',
|
||||
'oauth.scope.journey:share.label': 'Gerenciar links de jornadas',
|
||||
'oauth.scope.journey:share.description':
|
||||
'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.errorTitle': 'Authorization Error', // en-fallback
|
||||
'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback
|
||||
'oauth.authorize.loginDescription':
|
||||
'{client} wants access to your TREK account. Please sign in first.', // en-fallback
|
||||
'oauth.authorize.loginDescription': '{client} wants access to your TREK account. Please sign in first.', // en-fallback
|
||||
'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback
|
||||
'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback
|
||||
'oauth.authorize.requestDescription':
|
||||
'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.requestDescription': '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.selectScope': 'Select at least one scope', // en-fallback
|
||||
'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // 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.permissionsRequested': 'Permissions requested', // en-fallback
|
||||
'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback
|
||||
'oauth.authorize.alwaysTool.listTrips':
|
||||
'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.listTrips': '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
|
||||
};
|
||||
export default oauth;
|
||||
|
||||
@@ -5,8 +5,7 @@ const packing: TranslationStrings = {
|
||||
'packing.empty': 'A lista de mala está vazia',
|
||||
'packing.import': 'Importar',
|
||||
'packing.importTitle': 'Importar lista de bagagem',
|
||||
'packing.importHint':
|
||||
'Um item por linha. Formato: Categoria, Nome, Peso (g), Bolsa, checked/unchecked (opcional)',
|
||||
'packing.importHint': 'Um item por linha. Formato: Categoria, Nome, Peso (g), Bolsa, checked/unchecked (opcional)',
|
||||
'packing.importPlaceholder':
|
||||
'Higiene, Escova de dentes\nRoupas, Camisetas, 200\nDocumentos, Passaporte, , Mala de mão\nEletrônicos, Carregador, 50, Mala, checked',
|
||||
'packing.importCsv': 'Carregar CSV/TXT',
|
||||
@@ -52,8 +51,7 @@ const packing: TranslationStrings = {
|
||||
'packing.addBag': 'Adicionar mala',
|
||||
'packing.changeCategory': 'Alterar categoria',
|
||||
'packing.confirm.clearChecked': 'Remover {count} item(ns) marcado(s)?',
|
||||
'packing.confirm.deleteCat':
|
||||
'Excluir a categoria "{name}" com {count} item(ns)?',
|
||||
'packing.confirm.deleteCat': 'Excluir a categoria "{name}" com {count} item(ns)?',
|
||||
'packing.defaultCategory': 'Outros',
|
||||
'packing.toast.saveError': 'Falha ao salvar',
|
||||
'packing.toast.deleteError': 'Falha ao excluir',
|
||||
|
||||
+13
-26
@@ -32,33 +32,20 @@ const perm: TranslationStrings = {
|
||||
'perm.action.collab_edit': 'Colaboração (notas, enquetes, chat)',
|
||||
'perm.action.share_manage': 'Gerenciar links de compartilhamento',
|
||||
'perm.actionHint.trip_create': 'Quem pode criar novas viagens',
|
||||
'perm.actionHint.trip_edit':
|
||||
'Quem pode alterar nome, datas, descrição e moeda da viagem',
|
||||
'perm.actionHint.trip_edit': 'Quem pode alterar nome, datas, descrição e moeda da viagem',
|
||||
'perm.actionHint.trip_delete': 'Quem pode excluir permanentemente uma viagem',
|
||||
'perm.actionHint.trip_archive':
|
||||
'Quem pode arquivar ou desarquivar uma viagem',
|
||||
'perm.actionHint.trip_cover_upload':
|
||||
'Quem pode enviar ou alterar a imagem de capa',
|
||||
'perm.actionHint.member_manage':
|
||||
'Quem pode convidar ou remover membros da viagem',
|
||||
'perm.actionHint.trip_archive': 'Quem pode arquivar ou desarquivar uma viagem',
|
||||
'perm.actionHint.trip_cover_upload': '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_edit':
|
||||
'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.place_edit':
|
||||
'Quem pode adicionar, editar ou excluir lugares',
|
||||
'perm.actionHint.day_edit':
|
||||
'Quem pode editar dias, notas dos dias e atribuições de lugares',
|
||||
'perm.actionHint.reservation_edit':
|
||||
'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',
|
||||
'perm.actionHint.file_edit': '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.place_edit': 'Quem pode adicionar, editar ou excluir lugares',
|
||||
'perm.actionHint.day_edit': 'Quem pode editar dias, notas dos dias e atribuições de lugares',
|
||||
'perm.actionHint.reservation_edit': '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;
|
||||
|
||||
@@ -6,13 +6,10 @@ const places: TranslationStrings = {
|
||||
'places.sidebarDrop': 'Solte para importar',
|
||||
'places.importFileHint':
|
||||
'Importe arquivos .gpx, .kml ou .kmz de ferramentas como Google My Maps, Google Earth ou um rastreador GPS.',
|
||||
'places.importFileDropHere':
|
||||
'Clique para selecionar um arquivo ou arraste e solte aqui',
|
||||
'places.importFileDropHere': 'Clique para selecionar um arquivo ou arraste e solte aqui',
|
||||
'places.importFileDropActive': 'Solte o arquivo para selecionar',
|
||||
'places.importFileUnsupported':
|
||||
'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.importFileUnsupported': '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.importFileError': 'Importação falhou',
|
||||
'places.importAllSkipped': 'Todos os lugares já estavam na viagem.',
|
||||
'places.gpxImported': '{count} lugares importados do GPX',
|
||||
@@ -30,16 +27,13 @@ const places: TranslationStrings = {
|
||||
'places.kmlKmzImported': '{count} lugares importados de KMZ/KML',
|
||||
'places.urlResolved': 'Lugar importado da URL',
|
||||
'places.importList': 'Importar lista',
|
||||
'places.kmlKmzSummaryValues':
|
||||
'Placemarks: {total} • Importados: {created} • Ignorados: {skipped}',
|
||||
'places.kmlKmzSummaryValues': 'Placemarks: {total} • Importados: {created} • Ignorados: {skipped}',
|
||||
'places.importGoogleList': 'Lista Google',
|
||||
'places.importNaverList': 'Lista Naver',
|
||||
'places.googleListHint':
|
||||
'Cole um link compartilhado de uma lista do Google Maps para importar todos os lugares.',
|
||||
'places.googleListHint': 'Cole um link compartilhado de uma lista do Google Maps para importar todos os lugares.',
|
||||
'places.googleListImported': '{count} lugares importados de "{list}"',
|
||||
'places.googleListError': 'Falha ao importar lista do Google Maps',
|
||||
'places.naverListHint':
|
||||
'Cole um link compartilhado de uma lista do Naver Maps para importar todos os lugares.',
|
||||
'places.naverListHint': 'Cole um link compartilhado de uma lista do Naver Maps para importar todos os lugares.',
|
||||
'places.naverListImported': '{count} lugares importados de "{list}"',
|
||||
'places.naverListError': 'Falha ao importar lista do Naver Maps',
|
||||
'places.viewDetails': 'Ver detalhes',
|
||||
@@ -76,8 +70,7 @@ const places: TranslationStrings = {
|
||||
'places.formNotes': 'Notas',
|
||||
'places.formNotesPlaceholder': 'Notas pessoais...',
|
||||
'places.formReservation': 'Reserva',
|
||||
'places.reservationNotesPlaceholder':
|
||||
'Notas da reserva, código de confirmação...',
|
||||
'places.reservationNotesPlaceholder': 'Notas da reserva, código de confirmação...',
|
||||
'places.mapsSearchPlaceholder': 'Buscar lugares...',
|
||||
'places.mapsSearchError': 'Falha na busca de lugares.',
|
||||
'places.loadingDetails': 'Carregando detalhes do lugar…',
|
||||
|
||||
@@ -7,8 +7,7 @@ const planner: TranslationStrings = {
|
||||
'planner.documents': 'Documentos',
|
||||
'planner.dayPlan': 'Plano do dia',
|
||||
'planner.reservations': 'Reservas',
|
||||
'planner.minTwoPlaces':
|
||||
'São necessários pelo menos 2 lugares com coordenadas',
|
||||
'planner.minTwoPlaces': 'São necessários pelo menos 2 lugares com coordenadas',
|
||||
'planner.noGeoPlaces': 'Nenhum lugar com coordenadas disponível',
|
||||
'planner.routeCalculated': 'Rota calculada',
|
||||
'planner.routeCalcFailed': 'Não foi possível calcular a rota',
|
||||
@@ -34,8 +33,7 @@ const planner: TranslationStrings = {
|
||||
'planner.resConfirmed': 'Reserva confirmada · ',
|
||||
'planner.notePlaceholder': 'Nota…',
|
||||
'planner.noteTimePlaceholder': 'Horário (opcional)',
|
||||
'planner.noteExamplePlaceholder':
|
||||
'ex.: metrô às 14:30 da estação central, barco do cais 7, pausa para almoço…',
|
||||
'planner.noteExamplePlaceholder': 'ex.: metrô às 14:30 da estação central, barco do cais 7, pausa para almoço…',
|
||||
'planner.totalCost': 'Custo total',
|
||||
'planner.searchPlaces': 'Buscar lugares…',
|
||||
'planner.allCategories': 'Todas as categorias',
|
||||
@@ -49,8 +47,7 @@ const planner: TranslationStrings = {
|
||||
'planner.route': 'Rota',
|
||||
'planner.optimize': 'Otimizar',
|
||||
'planner.openGoogleMaps': 'Abrir no Google Maps',
|
||||
'planner.selectDayHint':
|
||||
'Selecione um dia na lista à esquerda para ver o plano do dia',
|
||||
'planner.selectDayHint': 'Selecione um dia na lista à esquerda para ver o plano do dia',
|
||||
'planner.noPlacesForDay': 'Nenhum lugar neste dia ainda',
|
||||
'planner.addPlacesLink': 'Adicionar lugares →',
|
||||
'planner.minTotal': 'mín. total',
|
||||
|
||||
@@ -6,8 +6,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.emptyHint': 'Adicione reservas de voos, hotéis e mais',
|
||||
'reservations.add': 'Adicionar reserva',
|
||||
'reservations.addManual': 'Reserva manual',
|
||||
'reservations.placeHint':
|
||||
'Dica: o ideal é criar reservas a partir de um lugar para vinculá-las ao plano do dia.',
|
||||
'reservations.placeHint': 'Dica: o ideal é criar reservas a partir de um lugar para vinculá-las ao plano do dia.',
|
||||
'reservations.confirmed': 'Confirmada',
|
||||
'reservations.pending': 'Pendente',
|
||||
'reservations.summary': '{confirmed} confirmada(s), {pending} pendente(s)',
|
||||
@@ -33,8 +32,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.layover.connection': 'Conexão',
|
||||
'reservations.layover.layover': 'Escala',
|
||||
'reservations.needsReview': 'Verificar',
|
||||
'reservations.needsReviewHint':
|
||||
'Aeroporto não pôde ser identificado automaticamente — confirme o local.',
|
||||
'reservations.needsReviewHint': 'Aeroporto não pôde ser identificado automaticamente — confirme o local.',
|
||||
'reservations.searchLocation': 'Buscar estação, porto, endereço...',
|
||||
'reservations.meta.trainNumber': 'Nº do trem',
|
||||
'reservations.meta.platform': 'Plataforma',
|
||||
@@ -64,8 +62,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.type.bicycle': 'Bicicleta',
|
||||
'reservations.type.taxi': 'Táxi',
|
||||
'reservations.type.transport_other': 'Outro',
|
||||
'reservations.confirm.delete':
|
||||
'Tem certeza de que deseja excluir a reserva "{name}"?',
|
||||
'reservations.confirm.delete': 'Tem certeza de que deseja excluir a reserva "{name}"?',
|
||||
'reservations.confirm.deleteTitle': 'Excluir reserva?',
|
||||
'reservations.confirm.deleteBody': '"{name}" será excluído permanentemente.',
|
||||
'reservations.toast.updated': 'Reserva atualizada',
|
||||
@@ -99,8 +96,7 @@ const reservations: TranslationStrings = {
|
||||
'reservations.budgetCategory': 'Categoria de orçamento',
|
||||
'reservations.budgetCategoryPlaceholder': 'ex. Transporte, Acomodação',
|
||||
'reservations.budgetCategoryAuto': 'Automático (pelo tipo de reserva)',
|
||||
'reservations.budgetHint':
|
||||
'Uma entrada de orçamento será criada automaticamente ao salvar.',
|
||||
'reservations.budgetHint': 'Uma entrada de orçamento será criada automaticamente ao salvar.',
|
||||
'reservations.departureDate': 'Partida',
|
||||
'reservations.arrivalDate': 'Chegada',
|
||||
'reservations.departureTime': 'Hora partida',
|
||||
@@ -121,60 +117,46 @@ const reservations: TranslationStrings = {
|
||||
'reservations.span.start': 'Início',
|
||||
'reservations.span.end': 'Fim',
|
||||
'reservations.span.ongoing': 'Em andamento',
|
||||
'reservations.validation.endBeforeStart':
|
||||
'A data/hora final deve ser posterior à data/hora inicial',
|
||||
'reservations.validation.endBeforeStart': 'A data/hora final deve ser posterior à data/hora inicial',
|
||||
'reservations.addBooking': 'Adicionar reserva',
|
||||
'reservations.import.title': 'Importar confirmações de reserva',
|
||||
'reservations.import.cta': 'Importar de arquivo',
|
||||
'reservations.import.dropHere':
|
||||
'Solte os arquivos de confirmação de reserva aqui ou clique para selecionar',
|
||||
'reservations.import.dropHere': 'Solte os arquivos de confirmação de reserva aqui ou clique para selecionar',
|
||||
'reservations.import.dropActive': 'Solte os arquivos para importar',
|
||||
'reservations.import.acceptedFormats':
|
||||
'Aceitos: EML, PDF, PKPass, HTML, TXT (máx. 10 MB cada, até 5 arquivos)',
|
||||
'reservations.import.acceptedFormats': 'Aceitos: EML, PDF, PKPass, HTML, TXT (máx. 10 MB cada, até 5 arquivos)',
|
||||
'reservations.import.parsing': 'Analisando arquivos…',
|
||||
'reservations.import.previewHeading': '{count} reserva(s) encontrada(s)',
|
||||
'reservations.import.previewEmpty':
|
||||
'Nenhuma reserva pôde ser extraída dos arquivos enviados.',
|
||||
'reservations.import.previewEmpty': 'Nenhuma reserva pôde ser extraída dos arquivos enviados.',
|
||||
'reservations.import.removeItem': 'Remover',
|
||||
'reservations.import.confirm': 'Importar {count} reserva(s)',
|
||||
'reservations.import.back': 'Voltar',
|
||||
'reservations.import.success': '{count} reserva(s) importada(s)',
|
||||
'reservations.import.partialFailure':
|
||||
'{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.unavailable':
|
||||
'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.import.partialFailure': '{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.unavailable': '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.cta': 'AirTrail',
|
||||
'reservations.airtrail.synced': 'AirTrail',
|
||||
'reservations.airtrail.syncedHint':
|
||||
'Sincronizado do AirTrail — as edições permanecem em sincronia nos dois sentidos.',
|
||||
'reservations.airtrail.notSynced': 'Não sincronizado',
|
||||
'reservations.airtrail.notSyncedHint':
|
||||
'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.notSyncedHint': '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.imported': '{count} voo(s) importado(s)',
|
||||
'reservations.airtrail.skippedDuplicate':
|
||||
'{count} já nesta viagem, ignorado(s)',
|
||||
'reservations.airtrail.skippedDuplicate': '{count} já nesta viagem, ignorado(s)',
|
||||
'reservations.airtrail.nothingImported': 'Nada para importar.',
|
||||
'reservations.airtrail.importError': 'Falha na importação. Tente novamente.',
|
||||
'reservations.airtrail.undo': 'Importar do AirTrail',
|
||||
'reservations.airtrail.alreadyImported': 'Importado',
|
||||
'reservations.airtrail.duringTrip': 'Durante esta viagem',
|
||||
'reservations.airtrail.otherFlights': 'Outros voos',
|
||||
'reservations.airtrail.empty':
|
||||
'Nenhum voo encontrado na sua conta do AirTrail.',
|
||||
'reservations.airtrail.empty': 'Nenhum voo encontrado na sua conta do AirTrail.',
|
||||
'reservations.airtrail.importCta': 'Importar {count}',
|
||||
'reservations.costsLabel': 'Costs',
|
||||
'reservations.createExpense': 'Create expense',
|
||||
'reservations.createExpenseHint':
|
||||
'Saves the booking, then opens the Costs editor.',
|
||||
'reservations.createExpenseHint': 'Saves the booking, then opens the Costs editor.',
|
||||
'reservations.linkedExpense': 'Linked expense',
|
||||
'reservations.removeExpense': 'Remove expense',
|
||||
};
|
||||
|
||||
@@ -14,12 +14,10 @@ const settings: TranslationStrings = {
|
||||
'settings.mapTemplate': 'Modelo de mapa',
|
||||
'settings.mapTemplatePlaceholder.select': 'Selecione o modelo...',
|
||||
'settings.mapDefaultHint': 'Deixe vazio para OpenStreetMap (padrão)',
|
||||
'settings.mapTemplatePlaceholder':
|
||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
'settings.mapHint': 'URL do modelo de blocos do mapa',
|
||||
'settings.mapProvider': 'Provedor de mapa',
|
||||
'settings.mapProviderHint':
|
||||
'Afeta os mapas do Planejador de Viagem e Diário. Atlas sempre usa Leaflet.',
|
||||
'settings.mapProviderHint': 'Afeta os mapas do Planejador de Viagem e Diário. Atlas sempre usa Leaflet.',
|
||||
'settings.mapLeafletSubtitle': 'Clássico 2D, quaisquer blocos raster',
|
||||
'settings.mapMapboxSubtitle': 'Blocos vetoriais, prédios 3D & terreno',
|
||||
'settings.mapExperimental': 'Experimental',
|
||||
@@ -30,13 +28,11 @@ const settings: TranslationStrings = {
|
||||
'settings.mapStylePlaceholder': 'Selecionar um estilo Mapbox',
|
||||
'settings.mapStyleHint': 'Preset ou sua própria URL mapbox://styles/USER/ID',
|
||||
'settings.map3dBuildings': 'Prédios 3D & terreno',
|
||||
'settings.map3dHint':
|
||||
'Inclinação + extrusões 3D reais de prédios — funciona em todo estilo, incluindo satélite.',
|
||||
'settings.map3dHint': 'Inclinação + extrusões 3D reais de prédios — funciona em todo estilo, incluindo satélite.',
|
||||
'settings.mapHighQuality': 'Modo alta qualidade',
|
||||
'settings.mapHighQualityHint':
|
||||
'Antialiasing + projeção global para bordas mais nítidas e uma visão realista do mundo.',
|
||||
'settings.mapHighQualityWarning':
|
||||
'Pode afetar o desempenho em dispositivos menos potentes.',
|
||||
'settings.mapHighQualityWarning': 'Pode afetar o desempenho em dispositivos menos potentes.',
|
||||
'settings.mapTipLabel': 'Dica:',
|
||||
'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).',
|
||||
@@ -45,11 +41,9 @@ const settings: TranslationStrings = {
|
||||
'settings.saveMap': 'Salvar mapa',
|
||||
'settings.apiKeys': 'Chaves de API',
|
||||
'settings.mapsKey': 'Chave da API Google Maps',
|
||||
'settings.mapsKeyHint':
|
||||
'Para busca de lugares. Requer Places API (New). Obtenha em console.cloud.google.com',
|
||||
'settings.mapsKeyHint': 'Para busca de lugares. Requer Places API (New). Obtenha em console.cloud.google.com',
|
||||
'settings.weatherKey': 'Chave OpenWeatherMap',
|
||||
'settings.weatherKeyHint':
|
||||
'Para dados meteorológicos. Grátis em openweathermap.org/api',
|
||||
'settings.weatherKeyHint': 'Para dados meteorológicos. Grátis em openweathermap.org/api',
|
||||
'settings.keyPlaceholder': 'Digite a chave...',
|
||||
'settings.configured': 'Configurada',
|
||||
'settings.saveKeys': 'Salvar chaves',
|
||||
@@ -78,8 +72,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationsDisabled':
|
||||
'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.notificationsManagedByAdmin':
|
||||
'Os eventos de notificação são configurados pelo administrador.',
|
||||
'settings.notificationsManagedByAdmin': 'Os eventos de notificação são configurados pelo administrador.',
|
||||
'settings.on': 'Ligado',
|
||||
'settings.off': 'Desligado',
|
||||
'settings.account': 'Conta',
|
||||
@@ -97,15 +90,13 @@ const settings: TranslationStrings = {
|
||||
'settings.about.supporters.tierEmpty': 'Seja o primeiro',
|
||||
'settings.about.supporter.tier.noReturnTicket': 'No Return Ticket',
|
||||
'settings.about.supporter.tier.lostLuggageVip': 'Lost Luggage VIP',
|
||||
'settings.about.supporter.tier.businessClassDreamer':
|
||||
'Business Class Dreamer',
|
||||
'settings.about.supporter.tier.businessClassDreamer': 'Business Class Dreamer',
|
||||
'settings.about.supporter.tier.budgetTraveller': 'Budget Traveller',
|
||||
'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate',
|
||||
'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.',
|
||||
'settings.about.madeWith': 'Feito com',
|
||||
'settings.about.madeBy':
|
||||
'por Maurice e uma crescente comunidade open-source.',
|
||||
'settings.about.madeBy': 'por Maurice e uma crescente comunidade open-source.',
|
||||
'settings.username': 'Nome de usuário',
|
||||
'settings.email': 'E-mail',
|
||||
'settings.role': 'Função',
|
||||
@@ -120,8 +111,7 @@ const settings: TranslationStrings = {
|
||||
'settings.passwordRequired': 'Informe a senha atual e a nova',
|
||||
'settings.passwordTooShort': 'A senha deve ter pelo menos 8 caracteres',
|
||||
'settings.passwordMismatch': 'As senhas não coincidem',
|
||||
'settings.passwordWeak':
|
||||
'A senha deve ter maiúscula, minúscula, número e um caractere especial',
|
||||
'settings.passwordWeak': 'A senha deve ter maiúscula, minúscula, número e um caractere especial',
|
||||
'settings.passwordChanged': 'Senha alterada com sucesso',
|
||||
'settings.deleteAccount': 'Excluir conta',
|
||||
'settings.deleteAccountTitle': 'Excluir sua conta?',
|
||||
@@ -148,10 +138,8 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.requiredByPolicy':
|
||||
'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.backupDescription':
|
||||
'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.backupDescription': '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.backupCopy': 'Copiar códigos',
|
||||
'settings.mfa.backupDownload': 'Baixar TXT',
|
||||
'settings.mfa.backupPrint': 'Imprimir / PDF',
|
||||
@@ -159,15 +147,13 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.enabled': 'O 2FA está ativado na sua conta.',
|
||||
'settings.mfa.disabled': 'O 2FA não está ativado.',
|
||||
'settings.mfa.setup': 'Configurar autenticador',
|
||||
'settings.mfa.scanQr':
|
||||
'Leia este QR code no app ou digite o segredo manualmente.',
|
||||
'settings.mfa.scanQr': 'Leia este QR code no app ou digite o segredo manualmente.',
|
||||
'settings.mfa.secretLabel': 'Chave secreta (entrada manual)',
|
||||
'settings.mfa.codePlaceholder': 'Código de 6 dígitos',
|
||||
'settings.mfa.enable': 'Ativar 2FA',
|
||||
'settings.mfa.cancelSetup': 'Cancelar',
|
||||
'settings.mfa.disableTitle': 'Desativar 2FA',
|
||||
'settings.mfa.disableHint':
|
||||
'Digite sua senha e um código atual do autenticador.',
|
||||
'settings.mfa.disableHint': 'Digite sua senha e um código atual do autenticador.',
|
||||
'settings.mfa.disable': 'Desativar 2FA',
|
||||
'settings.mfa.toastEnabled': 'Autenticação em duas etapas ativada',
|
||||
'settings.mfa.toastDisabled': 'Autenticação em duas etapas desativada',
|
||||
@@ -183,8 +169,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mcp.copied': 'Copiado!',
|
||||
'settings.mcp.apiTokens': 'Tokens de API',
|
||||
'settings.mcp.createToken': 'Criar novo token',
|
||||
'settings.mcp.noTokens':
|
||||
'Nenhum token ainda. Crie um para conectar clientes MCP.',
|
||||
'settings.mcp.noTokens': 'Nenhum token ainda. Crie um para conectar clientes MCP.',
|
||||
'settings.mcp.tokenCreatedAt': 'Criado em',
|
||||
'settings.mcp.tokenUsedAt': 'Usado em',
|
||||
'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.',
|
||||
'settings.mcp.modal.createTitle': 'Criar token de API',
|
||||
'settings.mcp.modal.tokenName': 'Nome do token',
|
||||
'settings.mcp.modal.tokenNamePlaceholder':
|
||||
'ex.: Claude Desktop, Notebook do trabalho',
|
||||
'settings.mcp.modal.tokenNamePlaceholder': 'ex.: Claude Desktop, Notebook do trabalho',
|
||||
'settings.mcp.modal.creating': 'Criando…',
|
||||
'settings.mcp.modal.create': 'Criar token',
|
||||
'settings.mcp.modal.createdTitle': 'Token criado',
|
||||
@@ -229,15 +213,13 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.sessionExpires': 'Expira',
|
||||
'settings.oauth.revoke': 'Revogar',
|
||||
'settings.oauth.revokeSession': 'Revogar sessão',
|
||||
'settings.oauth.revokeSessionMessage':
|
||||
'Isso revogará imediatamente o acesso desta sessão OAuth.',
|
||||
'settings.oauth.revokeSessionMessage': 'Isso revogará imediatamente o acesso desta sessão OAuth.',
|
||||
'settings.oauth.modal.createTitle': 'Registrar cliente OAuth',
|
||||
'settings.oauth.modal.presets': 'Configurações rápidas',
|
||||
'settings.oauth.modal.clientName': 'Nome da aplicação',
|
||||
'settings.oauth.modal.clientNamePlaceholder': 'ex.: Claude Web, Meu app MCP',
|
||||
'settings.oauth.modal.redirectUris': 'URIs de redirecionamento',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder':
|
||||
'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
|
||||
'settings.oauth.modal.redirectUrisHint':
|
||||
'Uma URI por linha. HTTPS obrigatório (localhost isento). Correspondência exata.',
|
||||
'settings.oauth.modal.scopes': 'Escopos permitidos',
|
||||
@@ -256,18 +238,15 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.toast.revoked': 'Sessão revogada',
|
||||
'settings.oauth.toast.revokeError': 'Falha ao revogar sessão',
|
||||
'settings.oauth.toast.rotateError': 'Falha ao renovar segredo do cliente',
|
||||
'settings.oauth.modal.machineClient':
|
||||
'Cliente de máquina (sem login no navegador)',
|
||||
'settings.oauth.modal.machineClient': 'Cliente de máquina (sem login no navegador)',
|
||||
'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.',
|
||||
'settings.oauth.modal.machineClientUsage':
|
||||
'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.mustChangePassword':
|
||||
'Você deve alterar sua senha antes de continuar. Defina uma nova senha abaixo.',
|
||||
'settings.mustChangePassword': 'Você deve alterar sua senha antes de continuar. Defina uma nova senha abaixo.',
|
||||
'settings.bookingLabels': 'Rótulos das rotas de reservas',
|
||||
'settings.bookingLabelsHint':
|
||||
'Mostra nomes de estações / aeroportos no mapa. Desativado, apenas o ícone aparece.',
|
||||
'settings.bookingLabelsHint': 'Mostra nomes de estações / aeroportos no mapa. Desativado, apenas o ícone aparece.',
|
||||
'settings.notifyVersionAvailable': 'Nova versão disponível',
|
||||
'settings.notificationPreferences.noChannels':
|
||||
'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.saved': 'Configurações do Ntfy salvas',
|
||||
'settings.ntfyUrl.test': 'Testar',
|
||||
'settings.ntfyUrl.testSuccess':
|
||||
'Notificação de teste do Ntfy enviada com sucesso',
|
||||
'settings.ntfyUrl.testSuccess': 'Notificação de teste do Ntfy enviada com sucesso',
|
||||
'settings.ntfyUrl.testFailed': 'Falha na notificação de teste do Ntfy',
|
||||
'settings.ntfyUrl.tokenCleared': 'Token de acesso removido',
|
||||
'settings.notificationPreferences.inapp': 'In-App',
|
||||
'settings.notificationPreferences.webhook': 'Webhook',
|
||||
'settings.notificationPreferences.email': 'Email',
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
"settings.currency": "Currency",
|
||||
"settings.currencyHint": "All amounts in Costs are converted to and shown in this currency.",
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'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.',
|
||||
@@ -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.',
|
||||
'settings.passkey.add': 'Adicionar uma passkey',
|
||||
'settings.passkey.addTitle': 'Adicionar uma passkey',
|
||||
'settings.passkey.passwordPrompt':
|
||||
'Confirme sua senha atual e depois siga as instruções do seu dispositivo.',
|
||||
'settings.passkey.passwordPrompt': 'Confirme sua senha atual e depois siga as instruções do seu dispositivo.',
|
||||
'settings.passkey.passwordRequired': 'A senha atual é obrigatória.',
|
||||
'settings.passkey.namePlaceholder': 'Nome (opcional, ex.: "iPhone")',
|
||||
'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.cancelled': 'Configuração da passkey cancelada',
|
||||
'settings.passkey.deleted': 'Passkey removida',
|
||||
'settings.passkey.deleteConfirm':
|
||||
'Remover esta passkey? Confirme com sua senha.',
|
||||
'settings.passkey.deleteConfirm': 'Remover esta passkey? Confirme com sua senha.',
|
||||
'settings.passkey.rename': 'Renomear',
|
||||
'settings.passkey.defaultName': 'Passkey',
|
||||
'settings.passkey.synced': 'Sincronizada',
|
||||
@@ -324,9 +300,11 @@ const settings: TranslationStrings = {
|
||||
'settings.passkey.lastUsed': 'Último uso',
|
||||
'settings.passkey.neverUsed': 'Nunca usada',
|
||||
'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.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.apiKey': 'Chave de API',
|
||||
'settings.airtrail.apiKeyPlaceholder': 'Chave de API Bearer',
|
||||
@@ -334,7 +312,8 @@ const settings: TranslationStrings = {
|
||||
'settings.airtrail.allowInsecureTls': 'Permitir certificados autoassinados',
|
||||
'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.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.notConnected': 'Não conectado',
|
||||
'settings.airtrail.toast.saved': 'Conexão com o AirTrail salva',
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { TranslationStrings } from '../types';
|
||||
|
||||
const shared: TranslationStrings = {
|
||||
'shared.expired': 'Link expirado ou inválido',
|
||||
'shared.expiredHint':
|
||||
'Este link de viagem compartilhado não está mais ativo.',
|
||||
'shared.expiredHint': 'Este link de viagem compartilhado não está mais ativo.',
|
||||
'shared.readOnly': 'Visualização somente leitura',
|
||||
'shared.tabPlan': 'Plano',
|
||||
'shared.tabBookings': 'Reservas',
|
||||
|
||||
@@ -5,12 +5,9 @@ const system_notice: TranslationStrings = {
|
||||
'system_notice.welcome_v1.body':
|
||||
'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.hero_alt':
|
||||
'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_share':
|
||||
'Colabore com seus companheiros de viagem',
|
||||
'system_notice.welcome_v1.hero_alt': '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_share': 'Colabore com seus companheiros de viagem',
|
||||
'system_notice.welcome_v1.highlight_offline': 'Funciona offline no celular',
|
||||
'system_notice.dev_test_modal.title': '[Dev] 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':
|
||||
'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.highlight_timeline':
|
||||
'Linha do tempo e galeria diária',
|
||||
'system_notice.v3_journey.highlight_timeline': 'Linha do tempo e galeria diária',
|
||||
'system_notice.v3_journey.highlight_photos': 'Importar do Immich ou Synology',
|
||||
'system_notice.v3_journey.highlight_share':
|
||||
'Compartilhar publicamente — sem login',
|
||||
'system_notice.v3_journey.highlight_export':
|
||||
'Exportar como álbum de fotos PDF',
|
||||
'system_notice.v3_journey.highlight_share': 'Compartilhar publicamente — sem login',
|
||||
'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.body':
|
||||
'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_offline':
|
||||
'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_features.body': '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_offline': '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.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.',
|
||||
'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_deprecated':
|
||||
'Tokens estáticos trek_ descontinuados',
|
||||
'system_notice.v3_mcp.highlight_tools':
|
||||
'Conjunto de ferramentas e prompts expandido',
|
||||
'system_notice.v3_mcp.highlight_deprecated': 'Tokens estáticos trek_ descontinuados',
|
||||
'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.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.',
|
||||
'system_notice.v3014_whitespace_collision.title':
|
||||
'Ação necessária: conflito de conta de usuário',
|
||||
'system_notice.v3014_whitespace_collision.title': 'Ação necessária: conflito de conta de usuário',
|
||||
'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.',
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const trip: TranslationStrings = {
|
||||
'trip.tabs.packingShort': 'Mala',
|
||||
'trip.tabs.lists': 'Listas',
|
||||
'trip.tabs.listsShort': 'Listas',
|
||||
'trip.tabs.budget': "Costs",
|
||||
'trip.tabs.budget': 'Costs',
|
||||
'trip.tabs.files': 'Arquivos',
|
||||
'trip.loading': 'Carregando viagem...',
|
||||
'trip.mobilePlan': 'Plano',
|
||||
|
||||
+13
-26
@@ -17,8 +17,7 @@ const vacay: TranslationStrings = {
|
||||
'vacay.editPerson': 'Editar pessoa',
|
||||
'vacay.removePerson': 'Remover pessoa',
|
||||
'vacay.removePersonConfirm': 'Remover {name}?',
|
||||
'vacay.removePersonHint':
|
||||
'Todas as entradas de férias desta pessoa serão excluídas permanentemente.',
|
||||
'vacay.removePersonHint': 'Todas as entradas de férias desta pessoa serão excluídas permanentemente.',
|
||||
'vacay.personName': 'Nome',
|
||||
'vacay.personNamePlaceholder': 'Digite o nome',
|
||||
'vacay.color': 'Cor',
|
||||
@@ -43,8 +42,7 @@ const vacay: TranslationStrings = {
|
||||
'vacay.fri': 'Sex',
|
||||
'vacay.sat': 'Sáb',
|
||||
'vacay.sun': 'Dom',
|
||||
'vacay.blockWeekendsHint':
|
||||
'Impedir entradas de férias aos sábados e domingos',
|
||||
'vacay.blockWeekendsHint': 'Impedir entradas de férias aos sábados e domingos',
|
||||
'vacay.publicHolidays': 'Feriados nacionais',
|
||||
'vacay.publicHolidaysHint': 'Marcar feriados nacionais no calendário',
|
||||
'vacay.selectCountry': 'Selecione o país',
|
||||
@@ -54,26 +52,20 @@ const vacay: TranslationStrings = {
|
||||
'vacay.calendarColor': 'Cor',
|
||||
'vacay.noCalendars': 'Nenhum calendário de feriados adicionado ainda',
|
||||
'vacay.companyHolidays': 'Feriados da empresa',
|
||||
'vacay.companyHolidaysHint':
|
||||
'Permitir marcar dias de feriado em toda a empresa',
|
||||
'vacay.companyHolidaysNoDeduct':
|
||||
'Feriados da empresa não contam como dias de férias.',
|
||||
'vacay.companyHolidaysHint': 'Permitir marcar dias de feriado em toda a empresa',
|
||||
'vacay.companyHolidaysNoDeduct': 'Feriados da empresa não contam como dias de férias.',
|
||||
'vacay.weekStart': 'Semana começa em',
|
||||
'vacay.weekStartHint':
|
||||
'Escolha se a semana começa na segunda-feira ou no domingo',
|
||||
'vacay.weekStartHint': 'Escolha se a semana começa na segunda-feira ou no domingo',
|
||||
'vacay.carryOver': 'Acúmulo',
|
||||
'vacay.carryOverHint':
|
||||
'Levar automaticamente os dias de férias restantes para o ano seguinte',
|
||||
'vacay.carryOverHint': 'Levar automaticamente os dias de férias restantes para o ano seguinte',
|
||||
'vacay.sharing': 'Compartilhamento',
|
||||
'vacay.sharingHint':
|
||||
'Compartilhe seu plano de férias com outros usuários do TREK',
|
||||
'vacay.sharingHint': 'Compartilhe seu plano de férias com outros usuários do TREK',
|
||||
'vacay.owner': 'Proprietário',
|
||||
'vacay.shareEmailPlaceholder': 'E-mail do usuário TREK',
|
||||
'vacay.shareSuccess': 'Plano compartilhado com sucesso',
|
||||
'vacay.shareError': 'Não foi possível compartilhar o plano',
|
||||
'vacay.dissolve': 'Encerrar fusão',
|
||||
'vacay.dissolveHint':
|
||||
'Separar os calendários novamente. Suas entradas serão mantidas.',
|
||||
'vacay.dissolveHint': 'Separar os calendários novamente. Suas entradas serão mantidas.',
|
||||
'vacay.dissolveAction': 'Encerrar',
|
||||
'vacay.dissolved': 'Calendário separado',
|
||||
'vacay.fusedWith': 'Fundido com',
|
||||
@@ -81,8 +73,7 @@ const vacay: TranslationStrings = {
|
||||
'vacay.noData': 'Sem dados',
|
||||
'vacay.changeColor': 'Alterar cor',
|
||||
'vacay.inviteUser': 'Convidar usuário',
|
||||
'vacay.inviteHint':
|
||||
'Convide outro usuário TREK para compartilhar um calendário de férias combinado.',
|
||||
'vacay.inviteHint': 'Convide outro usuário TREK para compartilhar um calendário de férias combinado.',
|
||||
'vacay.selectUser': 'Selecionar usuário',
|
||||
'vacay.sendInvite': 'Enviar convite',
|
||||
'vacay.inviteSent': 'Convite enviado',
|
||||
@@ -93,15 +84,11 @@ const vacay: TranslationStrings = {
|
||||
'vacay.decline': 'Recusar',
|
||||
'vacay.acceptFusion': 'Aceitar e fundir',
|
||||
'vacay.inviteTitle': 'Pedido de fusão',
|
||||
'vacay.inviteWantsToFuse':
|
||||
'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.inviteWantsToFuse': '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.fuseInfo2': 'Ambos podem criar e editar entradas um do outro.',
|
||||
'vacay.fuseInfo3':
|
||||
'Ambos podem excluir entradas e alterar direitos de férias.',
|
||||
'vacay.fuseInfo4':
|
||||
'Configurações como feriados nacionais e da empresa são compartilhadas.',
|
||||
'vacay.fuseInfo3': 'Ambos podem excluir entradas e alterar direitos de férias.',
|
||||
'vacay.fuseInfo4': 'Configurações como feriados nacionais e da empresa são compartilhadas.',
|
||||
'vacay.fuseInfo5':
|
||||
'A fusão pode ser encerrada a qualquer momento por qualquer parte. Suas entradas serão preservadas.',
|
||||
};
|
||||
|
||||
+49
-92
@@ -2,8 +2,7 @@ import type { TranslationStrings } from '../types';
|
||||
|
||||
const admin: TranslationStrings = {
|
||||
'admin.notifications.title': 'Oznámení',
|
||||
'admin.notifications.hint':
|
||||
'Vyberte kanál oznámení. Současně může být aktivní pouze jeden.',
|
||||
'admin.notifications.hint': 'Vyberte kanál oznámení. Současně může být aktivní pouze jeden.',
|
||||
'admin.notifications.none': 'Vypnuto',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
@@ -11,13 +10,11 @@ const admin: TranslationStrings = {
|
||||
'admin.notifications.saved': 'Nastavení oznámení uloženo',
|
||||
'admin.notifications.testWebhook': 'Odeslat testovací webhook',
|
||||
'admin.notifications.testWebhookSuccess': 'Testovací webhook úspěšně odeslán',
|
||||
'admin.notifications.testWebhookFailed':
|
||||
'Odeslání testovacího webhooku se nezdařilo',
|
||||
'admin.notifications.testWebhookFailed': 'Odeslání testovacího webhooku se nezdařilo',
|
||||
'admin.smtp.title': 'E-mail a oznámení',
|
||||
'admin.smtp.hint': 'Konfigurace SMTP pro odesílání e-mailových oznámení.',
|
||||
'admin.smtp.testButton': 'Odeslat testovací e-mail',
|
||||
'admin.webhook.hint':
|
||||
'Odesílat oznámení na externí webhook (Discord, Slack atd.).',
|
||||
'admin.webhook.hint': 'Odesílat oznámení na externí webhook (Discord, Slack atd.).',
|
||||
'admin.smtp.testSuccess': 'Testovací e-mail byl úspěšně odeslán',
|
||||
'admin.smtp.testFailed': 'Odeslání testovacího e-mailu se nezdařilo',
|
||||
'admin.title': 'Administrace',
|
||||
@@ -40,8 +37,7 @@ const admin: TranslationStrings = {
|
||||
'admin.editUser': 'Upravit uživatele',
|
||||
'admin.newPassword': 'Nové heslo',
|
||||
'admin.newPasswordHint': 'Ponechte prázdné pro zachování současného hesla',
|
||||
'admin.deleteUser':
|
||||
'Smazat uživatele „{name}“? Všechny jeho cesty budou trvale smazány.',
|
||||
'admin.deleteUser': 'Smazat uživatele „{name}“? Všechny jeho cesty budou trvale smazány.',
|
||||
'admin.deleteUserTitle': 'Smazat uživatele',
|
||||
'admin.newPasswordPlaceholder': 'Zadejte nové heslo…',
|
||||
'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.userCreated': 'Uživatel byl vytvořen',
|
||||
'admin.toast.createError': 'Nepodařilo se vytvořit uživatele',
|
||||
'admin.toast.fieldsRequired':
|
||||
'Uživatelské jméno, e-mail a heslo jsou povinné',
|
||||
'admin.toast.fieldsRequired': 'Uživatelské jméno, e-mail a heslo jsou povinné',
|
||||
'admin.createUser': 'Vytvořit uživatele',
|
||||
'admin.invite.title': 'Pozvánky',
|
||||
'admin.invite.subtitle': 'Vytvářejte jednorázové registrační odkazy',
|
||||
@@ -80,25 +75,20 @@ const admin: TranslationStrings = {
|
||||
'admin.passwordLogin': 'Password Login',
|
||||
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
|
||||
'admin.passwordRegistration': 'Password Registration',
|
||||
'admin.passwordRegistrationHint':
|
||||
'Allow new users to register with email and password',
|
||||
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
|
||||
'admin.oidcLogin': 'SSO Login',
|
||||
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
|
||||
'admin.oidcRegistration': 'SSO Auto-Provisioning',
|
||||
'admin.oidcRegistrationHint':
|
||||
'Automatically create accounts for new SSO users',
|
||||
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
|
||||
'admin.envOverrideHint':
|
||||
'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.requireMfa': 'Vyžadovat dvoufázové ověření (2FA)',
|
||||
'admin.requireMfaHint':
|
||||
'Uživatelé bez 2FA musí dokončit nastavení v Nastavení před použitím aplikace.',
|
||||
'admin.requireMfaHint': 'Uživatelé bez 2FA musí dokončit nastavení v Nastavení před použitím aplikace.',
|
||||
'admin.apiKeys': 'API klíče',
|
||||
'admin.apiKeysHint':
|
||||
'Volitelné. Povoluje rozšířená data o místech (fotky, počasí).',
|
||||
'admin.apiKeysHint': 'Volitelné. Povoluje rozšířená data o místech (fotky, počasí).',
|
||||
'admin.mapsKey': 'Google Maps API klíč',
|
||||
'admin.mapsKeyHint':
|
||||
'Povinné pro hledání míst. Získáte na console.cloud.google.com',
|
||||
'admin.mapsKeyHint': 'Povinné pro hledání míst. Získáte na console.cloud.google.com',
|
||||
'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.',
|
||||
'admin.recommended': 'Doporučeno',
|
||||
@@ -109,21 +99,17 @@ const admin: TranslationStrings = {
|
||||
'admin.keyInvalid': 'Neplatný',
|
||||
'admin.keySaved': 'API klíče byly uloženy',
|
||||
'admin.oidcTitle': 'Jednotné přihlášení (OIDC)',
|
||||
'admin.oidcSubtitle':
|
||||
'Povolit přihlášení přes externí poskytovatele (Google, Apple, Authentik, Keycloak).',
|
||||
'admin.oidcSubtitle': 'Povolit přihlášení přes externí poskytovatele (Google, Apple, Authentik, Keycloak).',
|
||||
'admin.oidcDisplayName': 'Zobrazované jméno',
|
||||
'admin.oidcIssuer': 'URL vydavatele (Issuer)',
|
||||
'admin.oidcIssuerHint':
|
||||
'OpenID Connect Issuer URL, např. https://accounts.google.com',
|
||||
'admin.oidcIssuerHint': 'OpenID Connect Issuer URL, např. https://accounts.google.com',
|
||||
'admin.oidcSaved': 'Konfigurace OIDC uložena',
|
||||
'admin.oidcOnlyMode': 'Zakázat ověřování heslem',
|
||||
'admin.oidcOnlyModeHint':
|
||||
'Pokud je zapnuto, je povolen pouze SSO login. Registrace i přihlášení heslem budou zablokovány.',
|
||||
'admin.fileTypes': 'Povolené typy souborů',
|
||||
'admin.fileTypesHint':
|
||||
'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.fileTypesHint': '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.fileTypesSaved': 'Nastavení souborů uloženo',
|
||||
'admin.placesPhotos.title': 'Fotografie míst',
|
||||
'admin.placesPhotos.subtitle':
|
||||
@@ -135,8 +121,7 @@ const admin: TranslationStrings = {
|
||||
'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.',
|
||||
'admin.bagTracking.title': 'Sledování zavazadel',
|
||||
'admin.bagTracking.subtitle':
|
||||
'Povolit váhu a přiřazení k zavazadlům u položek balení',
|
||||
'admin.bagTracking.subtitle': 'Povolit váhu a přiřazení k zavazadlům u položek balení',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
'admin.collab.chat.subtitle': 'Zasílání zpráv v reálném čase',
|
||||
'admin.collab.notes.title': 'Poznámky',
|
||||
@@ -155,11 +140,9 @@ const admin: TranslationStrings = {
|
||||
'admin.defaultSettings.resetToBuiltIn': 'obnovit',
|
||||
'admin.tabs.templates': 'Šablony seznamů',
|
||||
'admin.packingTemplates.title': 'Šablony pro balení',
|
||||
'admin.packingTemplates.subtitle':
|
||||
'Vytvářejte opakovaně použitelné seznamy pro své cesty',
|
||||
'admin.packingTemplates.subtitle': 'Vytvářejte opakovaně použitelné seznamy pro své cesty',
|
||||
'admin.packingTemplates.create': 'Nová šablona',
|
||||
'admin.packingTemplates.namePlaceholder':
|
||||
'Název šablony (např. Dovolená u moře)',
|
||||
'admin.packingTemplates.namePlaceholder': 'Název šablony (např. Dovolená u moře)',
|
||||
'admin.packingTemplates.empty': 'Zatím nejsou vytvořeny žádné šablony',
|
||||
'admin.packingTemplates.items': 'položek',
|
||||
'admin.packingTemplates.categories': 'kategorií',
|
||||
@@ -177,26 +160,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.title': 'Doplňky',
|
||||
'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.description':
|
||||
'Sdílejte cestovní fotky přes vaši instanci Immich',
|
||||
'admin.addons.catalog.memories.description': 'Sdílejte cestovní fotky přes vaši instanci Immich',
|
||||
'admin.addons.catalog.packing.name': 'Seznamy',
|
||||
'admin.addons.catalog.packing.description':
|
||||
'Balicí seznamy a úkoly pro vaše výlety',
|
||||
'admin.addons.catalog.packing.description': 'Balicí seznamy a úkoly pro vaše výlety',
|
||||
'admin.addons.catalog.budget.name': 'Rozpočet',
|
||||
'admin.addons.catalog.budget.description':
|
||||
'Sledování výdajů a plánování rozpočtu cesty',
|
||||
'admin.addons.catalog.budget.description': 'Sledování výdajů a plánování rozpočtu cesty',
|
||||
'admin.addons.catalog.documents.name': 'Dokumenty',
|
||||
'admin.addons.catalog.documents.description':
|
||||
'Ukládání a správa cestovních dokladů',
|
||||
'admin.addons.catalog.documents.description': 'Ukládání a správa cestovních dokladů',
|
||||
'admin.addons.catalog.vacay.name': 'Dovolená (Vacay)',
|
||||
'admin.addons.catalog.vacay.description':
|
||||
'Osobní plánovač dovolené s kalendářem',
|
||||
'admin.addons.catalog.vacay.description': 'Osobní plánovač dovolené s kalendářem',
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description':
|
||||
'Mapa světa s navštívenými zeměmi a statistikami',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa světa s navštívenými zeměmi a statistikami',
|
||||
'admin.addons.catalog.collab.name': 'Spolupráce',
|
||||
'admin.addons.catalog.collab.description':
|
||||
'Poznámky v reálném čase, hlasování a chat pro plánování',
|
||||
'admin.addons.catalog.collab.description': 'Poznámky v reálném čase, hlasování a chat pro plánování',
|
||||
'admin.addons.enabled': 'Povoleno',
|
||||
'admin.addons.disabled': 'Zakázáno',
|
||||
'admin.addons.type.trip': 'Cesta',
|
||||
@@ -204,20 +180,16 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.type.integration': 'Integrace',
|
||||
'admin.addons.tripHint': 'Dostupné jako karta v rámci každé cesty',
|
||||
'admin.addons.globalHint': 'Dostupné jako samostatná sekce v hlavní navigaci',
|
||||
'admin.addons.integrationHint':
|
||||
'Backendové služby a API integrace bez vlastní stránky',
|
||||
'admin.addons.integrationHint': 'Backendové služby a API integrace bez vlastní stránky',
|
||||
'admin.addons.toast.updated': 'Doplněk byl aktualizován',
|
||||
'admin.addons.toast.error': 'Aktualizace doplňku se nezdařila',
|
||||
'admin.addons.noAddons': 'Žádné doplňky nejsou k dispozici',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description':
|
||||
'Model Context Protocol pro integraci AI asistentů',
|
||||
'admin.addons.subtitleBefore':
|
||||
'Zapněte nebo vypněte funkce a přizpůsobte si ',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol pro integraci AI asistentů',
|
||||
'admin.addons.subtitleBefore': 'Zapněte nebo vypněte funkce a přizpůsobte si ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.tabs.audit': 'Audit',
|
||||
'admin.audit.subtitle':
|
||||
'Bezpečnostní a administrátorské události (zálohy, uživatelé, 2FA, nastavení).',
|
||||
'admin.audit.subtitle': 'Bezpečnostní a administrátorské události (zálohy, uživatelé, 2FA, nastavení).',
|
||||
'admin.audit.empty': 'Zatím žádné záznamy auditu.',
|
||||
'admin.audit.refresh': 'Obnovit',
|
||||
'admin.audit.loadMore': 'Načíst další',
|
||||
@@ -230,8 +202,7 @@ const admin: TranslationStrings = {
|
||||
'admin.audit.col.details': 'Detaily',
|
||||
'admin.tabs.mcpTokens': 'MCP přístup',
|
||||
'admin.mcpTokens.title': 'MCP přístup',
|
||||
'admin.mcpTokens.subtitle':
|
||||
'Správa OAuth relací a API tokenů všech uživatelů',
|
||||
'admin.mcpTokens.subtitle': 'Správa OAuth relací a API tokenů všech uživatelů',
|
||||
'admin.mcpTokens.sectionTitle': 'API tokeny',
|
||||
'admin.mcpTokens.owner': 'Vlastník',
|
||||
'admin.mcpTokens.tokenName': 'Název tokenu',
|
||||
@@ -252,8 +223,7 @@ const admin: TranslationStrings = {
|
||||
'admin.oauthSessions.created': 'Vytvořeno',
|
||||
'admin.oauthSessions.empty': 'Žádné aktivní OAuth relace',
|
||||
'admin.oauthSessions.revokeTitle': 'Zrušit relaci',
|
||||
'admin.oauthSessions.revokeMessage':
|
||||
'Tato OAuth relace bude okamžitě zrušena. Klient ztratí přístup k MCP.',
|
||||
'admin.oauthSessions.revokeMessage': 'Tato OAuth relace bude okamžitě zrušena. Klient ztratí přístup k MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Relace zrušena',
|
||||
'admin.oauthSessions.revokeError': 'Nepodařilo se zrušit relaci',
|
||||
'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.forecastDesc': 'Dříve 5 dní (OpenWeatherMap)',
|
||||
'admin.weather.climate': 'Historická klimatická data',
|
||||
'admin.weather.climateDesc':
|
||||
'Průměry za posledních 85 let pro dny mimo 16denní předpověď',
|
||||
'admin.weather.climateDesc': 'Průměry za posledních 85 let pro dny mimo 16denní předpověď',
|
||||
'admin.weather.requests': '10 000 požadavků denně',
|
||||
'admin.weather.requestsDesc': 'Zdarma, bez nutnosti klíče',
|
||||
'admin.weather.locationHint':
|
||||
'Počasí se určuje podle prvního místa se souřadnicemi v daném dni.',
|
||||
'admin.weather.locationHint': 'Počasí se určuje podle prvního místa se souřadnicemi v daném dni.',
|
||||
'admin.update.available': 'Dostupná aktualizace',
|
||||
'admin.update.text':
|
||||
'TREK {version} je k dispozici. Aktuálně používáte verzi {current}.',
|
||||
'admin.update.text': 'TREK {version} je k dispozici. Aktuálně používáte verzi {current}.',
|
||||
'admin.update.button': 'Zobrazit na GitHubu',
|
||||
'admin.update.install': 'Instalovat aktualizaci',
|
||||
'admin.update.confirmTitle': 'Instalovat aktualizaci?',
|
||||
'admin.update.confirmText':
|
||||
'TREK bude aktualizován z verze {current} na {version}. Server se poté automaticky restartuje.',
|
||||
'admin.update.dataInfo':
|
||||
'Všechna vaše data (cesty, uživatelé, API klíče, soubory) budou zachována.',
|
||||
'admin.update.dataInfo': '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.confirm': 'Aktualizovat nyní',
|
||||
'admin.update.installing': 'Aktualizace probíhá…',
|
||||
'admin.update.success':
|
||||
'Aktualizace byla nainstalována! Server se restartuje…',
|
||||
'admin.update.success': 'Aktualizace byla nainstalována! Server se restartuje…',
|
||||
'admin.update.failed': 'Aktualizace se nezdařila',
|
||||
'admin.update.backupHint': 'Před aktualizací doporučujeme vytvořit zálohu.',
|
||||
'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.webhookPanel.title': 'Webhook',
|
||||
'admin.notifications.inappPanel.title': 'In-App',
|
||||
'admin.notifications.inappPanel.hint':
|
||||
'In-app oznámení jsou vždy aktivní a nelze je globálně vypnout.',
|
||||
'admin.notifications.inappPanel.hint': 'In-app oznámení jsou vždy aktivní a nelze je globálně vypnout.',
|
||||
'admin.notifications.adminWebhookPanel.title': 'Admin webhook',
|
||||
'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.',
|
||||
'admin.notifications.adminWebhookPanel.saved': 'URL admin webhooku uložena',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess':
|
||||
'Testovací webhook byl úspěšně odeslán',
|
||||
'admin.notifications.adminWebhookPanel.testFailed':
|
||||
'Testovací webhook selhal',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint':
|
||||
'Admin webhook odesílá automaticky, pokud je nastavena URL',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess': 'Testovací webhook byl úspěšně odeslán',
|
||||
'admin.notifications.adminWebhookPanel.testFailed': 'Testovací webhook selhal',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin webhook odesílá automaticky, pokud je nastavena URL',
|
||||
'admin.notifications.ntfy': 'Ntfy',
|
||||
'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ů.',
|
||||
@@ -335,29 +296,23 @@ const admin: TranslationStrings = {
|
||||
'admin.notifications.adminNtfyPanel.serverPlaceholder': 'https://ntfy.sh',
|
||||
'admin.notifications.adminNtfyPanel.topicLabel': 'Admin téma',
|
||||
'admin.notifications.adminNtfyPanel.topicPlaceholder': 'trek-admin-alerts',
|
||||
'admin.notifications.adminNtfyPanel.tokenLabel':
|
||||
'Přístupový token (volitelné)',
|
||||
'admin.notifications.adminNtfyPanel.tokenCleared':
|
||||
'Přístupový token admina byl vymazán',
|
||||
'admin.notifications.adminNtfyPanel.tokenLabel': 'Přístupový token (volitelné)',
|
||||
'admin.notifications.adminNtfyPanel.tokenCleared': 'Přístupový token admina byl vymazán',
|
||||
'admin.notifications.adminNtfyPanel.saved': 'Nastavení admin Ntfy uloženo',
|
||||
'admin.notifications.adminNtfyPanel.test': 'Odeslat testovací Ntfy',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess':
|
||||
'Testovací Ntfy bylo úspěšně odesláno',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess': 'Testovací Ntfy bylo úspěšně odesláno',
|
||||
'admin.notifications.adminNtfyPanel.testFailed': 'Testovací Ntfy selhalo',
|
||||
'admin.notifications.adminNtfyPanel.alwaysOnHint':
|
||||
'Admin Ntfy odesílá vždy, když je nakonfigurováno téma',
|
||||
'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin Ntfy odesílá vždy, když je nakonfigurováno téma',
|
||||
'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.',
|
||||
'admin.notifications.tripReminders.title': 'Připomínky výletů',
|
||||
'admin.notifications.tripReminders.hint':
|
||||
'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.disabled':
|
||||
'Připomínky výletů deaktivovány',
|
||||
'admin.notifications.tripReminders.disabled': 'Připomínky výletů deaktivovány',
|
||||
'admin.tabs.notifications': 'Oznámení',
|
||||
'admin.addons.catalog.journey.name': 'Cestovní deník',
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Sledování cest a cestovní deník s odbaveními, fotkami a denními příběhy',
|
||||
'admin.addons.catalog.journey.description': '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.cardHint':
|
||||
'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.resetDone': 'Odebráno {count} přístupových klíčů',
|
||||
'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.providerMapbox': 'Mapbox (3D)',
|
||||
'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.mapboxStylePlaceholder': 'Vyberte styl…',
|
||||
'admin.defaultSettings.mapbox3d': '3D budovy & terén',
|
||||
|
||||
@@ -28,8 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.visitedCountries': 'Navštívené země',
|
||||
'atlas.cities': 'Města',
|
||||
'atlas.noData': 'Zatím žádná cestovatelská data',
|
||||
'atlas.noDataHint':
|
||||
'Vytvořte cestu a přidejte místa, abyste viděli svou mapu světa',
|
||||
'atlas.noDataHint': 'Vytvořte cestu a přidejte místa, abyste viděli svou mapu světa',
|
||||
'atlas.lastTrip': 'Poslední cesta',
|
||||
'atlas.nextTrip': 'Další cesta',
|
||||
'atlas.daysLeft': 'dní zbývá',
|
||||
|
||||
@@ -12,10 +12,8 @@ const backup: TranslationStrings = {
|
||||
'backup.createFirst': 'Vytvořit první zálohu',
|
||||
'backup.download': 'Stáhnout',
|
||||
'backup.restore': 'Obnovit',
|
||||
'backup.confirm.restore':
|
||||
'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.restore': '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.delete': 'Smazat zálohu „{name}"?',
|
||||
'backup.toast.loadError': 'Nepodařilo se načíst zálohy',
|
||||
'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.subtitle': 'Automatické zálohování podle plánu',
|
||||
'backup.auto.enable': 'Povolit automatické zálohování',
|
||||
'backup.auto.enableHint':
|
||||
'Zálohy budou vytvářeny automaticky podle zvoleného plánu',
|
||||
'backup.auto.enableHint': 'Zálohy budou vytvářeny automaticky podle zvoleného plánu',
|
||||
'backup.auto.interval': 'Interval',
|
||||
'backup.auto.hour': 'Spustit v hodinu',
|
||||
'backup.auto.hourHint': 'Místní čas serveru (formát {format})',
|
||||
'backup.auto.dayOfWeek': 'Den v týdnu',
|
||||
'backup.auto.dayOfMonth': 'Den v měsíci',
|
||||
'backup.auto.dayOfMonthHint':
|
||||
'Omezeno na 1–28 pro kompatibilitu se všemi měsíci',
|
||||
'backup.auto.dayOfMonthHint': 'Omezeno na 1–28 pro kompatibilitu se všemi měsíci',
|
||||
'backup.auto.scheduleSummary': 'Plán',
|
||||
'backup.auto.summaryDaily': 'Každý den v {hour}:00',
|
||||
'backup.auto.summaryWeekly': 'Každý {day} v {hour}:00',
|
||||
|
||||
@@ -4,8 +4,7 @@ const budget: TranslationStrings = {
|
||||
'budget.title': 'Rozpočet',
|
||||
'budget.exportCsv': 'Exportovat CSV',
|
||||
'budget.emptyTitle': 'Zatím nebyl vytvořen žádný rozpočet',
|
||||
'budget.emptyText':
|
||||
'Vytvořte kategorie a položky pro plánování cestovního rozpočtu',
|
||||
'budget.emptyText': 'Vytvořte kategorie a položky pro plánování cestovního rozpočtu',
|
||||
'budget.emptyPlaceholder': 'Zadejte název kategorie...',
|
||||
'budget.createCategory': 'Vytvořit kategorii',
|
||||
'budget.category': 'Kategorie',
|
||||
@@ -27,8 +26,7 @@ const budget: TranslationStrings = {
|
||||
'budget.byCategory': 'Podle kategorie',
|
||||
'budget.editTooltip': 'Klikněte pro úpravu',
|
||||
'budget.linkedToReservation': 'Propojeno s rezervací — název upravte tam',
|
||||
'budget.confirm.deleteCategory':
|
||||
'Opravdu chcete smazat kategorii „{name}” s {count} položkami?',
|
||||
'budget.confirm.deleteCategory': 'Opravdu chcete smazat kategorii „{name}” s {count} položkami?',
|
||||
'budget.deleteCategory': 'Smazat kategorii',
|
||||
'budget.perPerson': 'Na osobu',
|
||||
'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ží.',
|
||||
'budget.netBalances': 'Čisté zůstatky',
|
||||
'budget.categoriesLabel': 'kategorie',
|
||||
"costs.you": "Vy",
|
||||
"costs.youShort": "Vy",
|
||||
"costs.youLower": "vy",
|
||||
"costs.youOwe": "Dlužíte",
|
||||
"costs.youOweSub": "Měli byste zaplatit ostatním",
|
||||
"costs.youreOwed": "Dluží vám",
|
||||
"costs.youreOwedSub": "Ostatní by měli zaplatit vám",
|
||||
"costs.totalSpend": "Celkové výdaje na cestu",
|
||||
"costs.totalSpendSub": "Za všechny cestovatele",
|
||||
"costs.to": "Komu",
|
||||
"costs.from": "Od",
|
||||
"costs.allSettled": "Máte vše vyrovnáno",
|
||||
"costs.nothingOwed": "Nikdo vám nic nedluží",
|
||||
"costs.yourShare": "Váš podíl",
|
||||
"costs.youPaid": "Zaplatili jste",
|
||||
"costs.expenses": "Výdaje",
|
||||
"costs.entries": "{count} položek",
|
||||
"costs.searchPlaceholder": "Hledat výdaje…",
|
||||
"costs.filter.all": "Vše",
|
||||
"costs.filter.mine": "Zaplaceno mnou",
|
||||
"costs.filter.owed": "Dluží mi",
|
||||
"costs.addExpense": "Přidat výdaj",
|
||||
"costs.editExpense": "Upravit výdaj",
|
||||
"costs.noMatch": "Žádné výdaje neodpovídají vašemu hledání.",
|
||||
"costs.emptyText": "Zatím žádné výdaje. Přidejte první.",
|
||||
"costs.spent": "Utraceno {amount}",
|
||||
"costs.noDate": "Bez data",
|
||||
"costs.noOnePaid": "Zatím nikdo nezaplatil",
|
||||
"costs.youLent": "půjčili jste {amount}",
|
||||
"costs.youBorrowed": "vypůjčili jste si {amount}",
|
||||
"costs.settleUp": "Vyrovnat",
|
||||
"costs.history": "Historie",
|
||||
"costs.everyoneSquare": "Všichni jsou vyrovnáni",
|
||||
"costs.nothingOutstanding": "Momentálně žádné nevyrovnané platby.",
|
||||
"costs.pay": "zaplatí",
|
||||
"costs.pays": "zaplatí",
|
||||
"costs.settle": "Vyrovnat",
|
||||
"costs.balances": "Zůstatky",
|
||||
"costs.byCategory": "Podle kategorie",
|
||||
"costs.noCategories": "Zatím žádné výdaje.",
|
||||
"costs.settleHistory": "Historie vyrovnání",
|
||||
"costs.noSettlements": "Zatím žádné vyrovnané platby.",
|
||||
"costs.paymentsSettled": "{count} plateb vyrovnáno",
|
||||
"costs.paid": "zaplaceno",
|
||||
"costs.undo": "Vrátit zpět",
|
||||
"costs.whatFor": "Za co to bylo?",
|
||||
"costs.namePlaceholder": "např. večeře, suvenýry, benzín…",
|
||||
"costs.totalAmount": "Celková částka",
|
||||
"costs.currency": "Měna",
|
||||
"costs.day": "Den",
|
||||
"costs.rateLabel": "1 {from} v {to}",
|
||||
"costs.category": "Kategorie",
|
||||
"costs.whoPaid": "Kdo zaplatil?",
|
||||
"costs.splitBetween": "Rozdělit rovným dílem mezi",
|
||||
"costs.pickSomeone": "Vyberte alespoň jednu osobu pro rozdělení.",
|
||||
"costs.splitSummary": "Rozděleno na {count} dílů · {amount} každý",
|
||||
"costs.cat.accommodation": "Ubytování",
|
||||
"costs.cat.food": "Jídlo a pití",
|
||||
"costs.cat.groceries": "Potraviny",
|
||||
"costs.cat.transport": "Doprava",
|
||||
"costs.cat.flights": "Lety",
|
||||
"costs.cat.activities": "Aktivity",
|
||||
"costs.cat.sightseeing": "Prohlídka památek",
|
||||
"costs.cat.shopping": "Nákupy",
|
||||
"costs.cat.fees": "Poplatky a vstupenky",
|
||||
"costs.cat.health": "Zdraví",
|
||||
"costs.cat.tips": "Spropitné",
|
||||
"costs.cat.other": "Ostatní",
|
||||
"costs.daysCount": "{count} dní",
|
||||
"costs.travelers": "{count} cestovatelů",
|
||||
"costs.liveRate": "aktuální kurz",
|
||||
"costs.settleAll": "Vyrovnat vše",
|
||||
'costs.you': 'Vy',
|
||||
'costs.youShort': 'Vy',
|
||||
'costs.youLower': 'vy',
|
||||
'costs.youOwe': 'Dlužíte',
|
||||
'costs.youOweSub': 'Měli byste zaplatit ostatním',
|
||||
'costs.youreOwed': 'Dluží vám',
|
||||
'costs.youreOwedSub': 'Ostatní by měli zaplatit vám',
|
||||
'costs.totalSpend': 'Celkové výdaje na cestu',
|
||||
'costs.totalSpendSub': 'Za všechny cestovatele',
|
||||
'costs.to': 'Komu',
|
||||
'costs.from': 'Od',
|
||||
'costs.allSettled': 'Máte vše vyrovnáno',
|
||||
'costs.nothingOwed': 'Nikdo vám nic nedluží',
|
||||
'costs.yourShare': 'Váš podíl',
|
||||
'costs.youPaid': 'Zaplatili jste',
|
||||
'costs.expenses': 'Výdaje',
|
||||
'costs.entries': '{count} položek',
|
||||
'costs.searchPlaceholder': 'Hledat výdaje…',
|
||||
'costs.filter.all': 'Vše',
|
||||
'costs.filter.mine': 'Zaplaceno mnou',
|
||||
'costs.filter.owed': 'Dluží mi',
|
||||
'costs.addExpense': 'Přidat výdaj',
|
||||
'costs.editExpense': 'Upravit výdaj',
|
||||
'costs.noMatch': 'Žádné výdaje neodpovídají vašemu hledání.',
|
||||
'costs.emptyText': 'Zatím žádné výdaje. Přidejte první.',
|
||||
'costs.spent': 'Utraceno {amount}',
|
||||
'costs.noDate': 'Bez data',
|
||||
'costs.noOnePaid': 'Zatím nikdo nezaplatil',
|
||||
'costs.youLent': 'půjčili jste {amount}',
|
||||
'costs.youBorrowed': 'vypůjčili jste si {amount}',
|
||||
'costs.settleUp': 'Vyrovnat',
|
||||
'costs.history': 'Historie',
|
||||
'costs.everyoneSquare': 'Všichni jsou vyrovnáni',
|
||||
'costs.nothingOutstanding': 'Momentálně žádné nevyrovnané platby.',
|
||||
'costs.pay': 'zaplatí',
|
||||
'costs.pays': 'zaplatí',
|
||||
'costs.settle': 'Vyrovnat',
|
||||
'costs.balances': 'Zůstatky',
|
||||
'costs.byCategory': 'Podle kategorie',
|
||||
'costs.noCategories': 'Zatím žádné výdaje.',
|
||||
'costs.settleHistory': 'Historie vyrovnání',
|
||||
'costs.noSettlements': 'Zatím žádné vyrovnané platby.',
|
||||
'costs.paymentsSettled': '{count} plateb vyrovnáno',
|
||||
'costs.paid': 'zaplaceno',
|
||||
'costs.undo': 'Vrátit zpět',
|
||||
'costs.whatFor': 'Za co to bylo?',
|
||||
'costs.namePlaceholder': 'např. večeře, suvenýry, benzín…',
|
||||
'costs.totalAmount': 'Celková částka',
|
||||
'costs.currency': 'Měna',
|
||||
'costs.day': 'Den',
|
||||
'costs.rateLabel': '1 {from} v {to}',
|
||||
'costs.category': 'Kategorie',
|
||||
'costs.whoPaid': 'Kdo zaplatil?',
|
||||
'costs.splitBetween': 'Rozdělit rovným dílem mezi',
|
||||
'costs.pickSomeone': 'Vyberte alespoň jednu osobu pro rozdělení.',
|
||||
'costs.splitSummary': 'Rozděleno na {count} dílů · {amount} každý',
|
||||
'costs.cat.accommodation': 'Ubytování',
|
||||
'costs.cat.food': 'Jídlo a pití',
|
||||
'costs.cat.groceries': 'Potraviny',
|
||||
'costs.cat.transport': 'Doprava',
|
||||
'costs.cat.flights': 'Lety',
|
||||
'costs.cat.activities': 'Aktivity',
|
||||
'costs.cat.sightseeing': 'Prohlídka památek',
|
||||
'costs.cat.shopping': 'Nákupy',
|
||||
'costs.cat.fees': 'Poplatky a vstupenky',
|
||||
'costs.cat.health': 'Zdraví',
|
||||
'costs.cat.tips': 'Spropitné',
|
||||
'costs.cat.other': 'Ostatní',
|
||||
'costs.daysCount': '{count} dní',
|
||||
'costs.travelers': '{count} cestovatelů',
|
||||
'costs.liveRate': 'aktuální kurz',
|
||||
'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;
|
||||
|
||||
@@ -13,8 +13,7 @@ const categories: TranslationStrings = {
|
||||
'categories.defaultName': 'Kategorie',
|
||||
'categories.update': 'Aktualizovat',
|
||||
'categories.create': 'Vytvořit',
|
||||
'categories.confirm.delete':
|
||||
'Smazat kategorii? Místa v této kategorii nebudou smazána.',
|
||||
'categories.confirm.delete': 'Smazat kategorii? Místa v této kategorii nebudou smazána.',
|
||||
'categories.toast.loadError': 'Nepodařilo se načíst kategorie',
|
||||
'categories.toast.nameRequired': 'Prosím zadejte název',
|
||||
'categories.toast.updated': 'Kategorie aktualizována',
|
||||
|
||||
@@ -14,8 +14,7 @@ const collab: TranslationStrings = {
|
||||
'collab.chat.placeholder': 'Napište zprávu...',
|
||||
'collab.chat.empty': 'Začněte konverzaci',
|
||||
'collab.chat.emptyHint': 'Zprávy jsou sdíleny se všemi členy cesty',
|
||||
'collab.chat.emptyDesc':
|
||||
'Sdílejte nápady, plány a novinky se svou cestovatelskou skupinou',
|
||||
'collab.chat.emptyDesc': 'Sdílejte nápady, plány a novinky se svou cestovatelskou skupinou',
|
||||
'collab.chat.today': 'Dnes',
|
||||
'collab.chat.yesterday': 'Včera',
|
||||
'collab.chat.deletedMessage': 'smazal zprávu',
|
||||
|
||||
@@ -20,8 +20,7 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.timezoneCustomTzPlaceholder': 'např. America/New_York',
|
||||
'dashboard.timezoneCustomAdd': 'Přidat',
|
||||
'dashboard.timezoneCustomErrorEmpty': 'Zadejte identifikátor pásma',
|
||||
'dashboard.timezoneCustomErrorInvalid':
|
||||
'Neplatné pásmo. Použijte formát jako např. Europe/Prague',
|
||||
'dashboard.timezoneCustomErrorInvalid': 'Neplatné pásmo. Použijte formát jako např. Europe/Prague',
|
||||
'dashboard.timezoneCustomErrorDuplicate': 'Již bylo přidáno',
|
||||
'dashboard.emptyTitle': 'Zatím žádné cesty',
|
||||
'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.copied': 'Cesta byla zkopírována!',
|
||||
'dashboard.toast.copyError': 'Nepodařilo se zkopírovat cestu',
|
||||
'dashboard.confirm.delete':
|
||||
'Smazat cestu „{title}”? Všechna místa a plány budou trvale smazány.',
|
||||
'dashboard.confirm.delete': 'Smazat cestu „{title}”? Všechna místa a plány budou trvale smazány.',
|
||||
'dashboard.editTrip': 'Upravit cestu',
|
||||
'dashboard.createTrip': 'Vytvořit novou cestu',
|
||||
'dashboard.tripTitle': 'Název',
|
||||
@@ -66,10 +64,8 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.startDate': 'Datum začátku',
|
||||
'dashboard.endDate': 'Datum konce',
|
||||
'dashboard.dayCount': 'Počet dnů',
|
||||
'dashboard.dayCountHint':
|
||||
'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.dayCountHint': '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.coverImage': 'Úvodní obrázek',
|
||||
'dashboard.addCoverImage': 'Vybrat úvodní obrázek (nebo přetáhnout sem)',
|
||||
'dashboard.addMembers': 'Spolucestující',
|
||||
|
||||
@@ -7,10 +7,8 @@ const day: TranslationStrings = {
|
||||
'day.sunrise': 'Východ slunce',
|
||||
'day.sunset': 'Západ slunce',
|
||||
'day.hourlyForecast': 'Hodinová předpověď',
|
||||
'day.climateHint':
|
||||
'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.climateHint': '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.overview': 'Denní přehled',
|
||||
'day.accommodation': 'Ubytování',
|
||||
'day.addAccommodation': 'Přidat ubytování',
|
||||
|
||||
@@ -17,30 +17,24 @@ const dayplan: TranslationStrings = {
|
||||
'dayplan.optimize': 'Optimalizovat',
|
||||
'dayplan.optimized': 'Trasa optimalizována',
|
||||
'dayplan.routeError': 'Nepodařilo se vypočítat trasu',
|
||||
'dayplan.toast.needTwoPlaces':
|
||||
'Pro optimalizaci trasy jsou potřeba alespoň dvě místa',
|
||||
'dayplan.toast.needTwoPlaces': 'Pro optimalizaci trasy jsou potřeba alespoň dvě místa',
|
||||
'dayplan.toast.routeOptimized': 'Trasa byla optimalizována',
|
||||
'dayplan.toast.routeOptimizedFromHotel':
|
||||
'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.routeOptimizedFromHotel': '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.confirmed': 'Potvrzeno',
|
||||
'dayplan.pendingRes': 'Čeká na potvrzení',
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'Exportovat denní plán do PDF',
|
||||
'dayplan.pdfError': 'Export do PDF se nezdařil',
|
||||
'dayplan.cannotReorderTransport':
|
||||
'Rezervace s pevným časem nelze přeuspořádat',
|
||||
'dayplan.cannotReorderTransport': 'Rezervace s pevným časem nelze přeuspořádat',
|
||||
'dayplan.confirmRemoveTimeTitle': 'Odebrat čas?',
|
||||
'dayplan.confirmRemoveTimeBody':
|
||||
'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.confirmDeleteNoteTitle': 'Smazat poznámku?',
|
||||
'dayplan.confirmDeleteNoteBody': 'Tato poznámka bude trvale smazána.',
|
||||
'dayplan.cannotDropOnTimed':
|
||||
'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.cannotDropOnTimed': '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.mobile.addPlace': 'Přidat místo',
|
||||
'dayplan.mobile.searchPlaces': 'Hledat místa...',
|
||||
'dayplan.mobile.allAssigned': 'Všechna místa přiřazena',
|
||||
|
||||
@@ -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.',
|
||||
ctaIntro: 'Obnovit heslo',
|
||||
expiry: 'Odkaz vyprší za 60 minut.',
|
||||
ignore:
|
||||
'Pokud jsi o obnovení nežádal/a, tento e-mail ignoruj — heslo zůstane beze změny.',
|
||||
ignore: 'Pokud jsi o obnovení nežádal/a, tento e-mail ignoruj — heslo zůstane beze změny.',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ const files: TranslationStrings = {
|
||||
'files.uploadError': 'Nahrávání se nezdařilo',
|
||||
'files.dropzone': 'Přetáhněte soubory sem',
|
||||
'files.dropzoneHint': 'nebo klikněte pro výběr',
|
||||
'files.allowedTypes':
|
||||
'Obrázky, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Max 50 MB',
|
||||
'files.allowedTypes': 'Obrázky, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Max 50 MB',
|
||||
'files.uploading': 'Nahrávání...',
|
||||
'files.filterAll': 'Vše',
|
||||
'files.filterPdf': 'PDF',
|
||||
@@ -52,10 +51,8 @@ const files: TranslationStrings = {
|
||||
'files.toast.assigned': 'Soubor byl přiřazen',
|
||||
'files.toast.assignError': 'Přiřazení se nezdařilo',
|
||||
'files.toast.restoreError': 'Obnovení se nezdařilo',
|
||||
'files.confirm.permanentDelete':
|
||||
'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.permanentDelete': '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.noteLabel': 'Poznámka',
|
||||
'files.notePlaceholder': 'Přidat poznámku...',
|
||||
};
|
||||
|
||||
@@ -14,14 +14,12 @@ const journey: TranslationStrings = {
|
||||
'journey.createError': 'Nepodařilo se vytvořit cestovní deník',
|
||||
'journey.deleteError': 'Nepodařilo se smazat cestovní deník',
|
||||
'journey.deleteConfirmTitle': 'Smazat',
|
||||
'journey.deleteConfirmMessage':
|
||||
'Smazat „{title}"? Tuto akci nelze vrátit zpět.',
|
||||
'journey.deleteConfirmMessage': 'Smazat „{title}"? Tuto akci nelze vrátit zpět.',
|
||||
'journey.deleteConfirmGeneric': 'Opravdu to chcete smazat?',
|
||||
'journey.notFound': 'Cestovní deník nenalezen',
|
||||
'journey.photos': 'Fotky',
|
||||
'journey.timelineEmpty': 'Zatím žádné zastávky',
|
||||
'journey.timelineEmptyHint':
|
||||
'Přidejte odbavení nebo napište záznam do deníku',
|
||||
'journey.timelineEmptyHint': 'Přidejte odbavení nebo napište záznam do deníku',
|
||||
'journey.status.draft': 'Koncept',
|
||||
'journey.status.active': 'Aktivní',
|
||||
'journey.status.completed': 'Dokončeno',
|
||||
@@ -47,30 +45,25 @@ const journey: TranslationStrings = {
|
||||
'journey.editor.titlePlaceholder': 'Pojmenujte tento okamžik...',
|
||||
'journey.editor.bodyPlaceholder': 'Vyprávějte příběh tohoto dne...',
|
||||
'journey.editor.placePlaceholder': 'Místo (volitelné)',
|
||||
'journey.editor.tagsPlaceholder':
|
||||
'Tagy: skrytý klenot, nejlepší jídlo, musím se vrátit...',
|
||||
'journey.editor.tagsPlaceholder': 'Tagy: skrytý klenot, nejlepší jídlo, musím se vrátit...',
|
||||
'journey.visibility.private': 'Soukromý',
|
||||
'journey.visibility.shared': 'Sdílený',
|
||||
'journey.visibility.public': 'Veřejný',
|
||||
'journey.emptyState.title': 'Váš příběh začíná zde',
|
||||
'journey.emptyState.subtitle':
|
||||
'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.emptyState.subtitle': '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.createJourney': 'Vytvořit cestovní deník',
|
||||
'journey.frontpage.activeJourney': 'Aktivní cestovní deník',
|
||||
'journey.frontpage.allJourneys': 'Všechny cestovní deníky',
|
||||
'journey.frontpage.journeys': 'cestovní deníky',
|
||||
'journey.frontpage.createNew': 'Vytvořit nový cestovní deník',
|
||||
'journey.frontpage.createNewSub':
|
||||
'Vyberte cesty, pište příběhy, sdílejte dobrodružství',
|
||||
'journey.frontpage.createNewSub': 'Vyberte cesty, pište příběhy, sdílejte dobrodružství',
|
||||
'journey.frontpage.live': 'Živě',
|
||||
'journey.frontpage.synced': 'Synchronizováno',
|
||||
'journey.frontpage.continueWriting': 'Pokračovat v psaní',
|
||||
'journey.frontpage.updated': 'Aktualizováno {time}',
|
||||
'journey.frontpage.suggestionLabel': 'Cesta právě skončila',
|
||||
'journey.frontpage.suggestionText':
|
||||
'Proměňte <strong>{title}</strong> v cestovní deník',
|
||||
'journey.frontpage.suggestionText': 'Proměňte <strong>{title}</strong> v cestovní deník',
|
||||
'journey.frontpage.dismiss': 'Zavřít',
|
||||
'journey.frontpage.journeyName': 'Název cestovního deníku',
|
||||
'journey.frontpage.namePlaceholder': 'např. Jihovýchodní Asie 2026',
|
||||
@@ -85,11 +78,9 @@ const journey: TranslationStrings = {
|
||||
'journey.detail.newEntry': 'Nový záznam',
|
||||
'journey.detail.editEntry': 'Upravit záznam',
|
||||
'journey.detail.noEntries': 'Zatím žádné záznamy',
|
||||
'journey.detail.noEntriesHint':
|
||||
'Přidejte cestu pro začátek s kostrovými záznamy',
|
||||
'journey.detail.noEntriesHint': 'Přidejte cestu pro začátek s kostrovými záznamy',
|
||||
'journey.detail.noPhotos': 'Zatím žádné fotky',
|
||||
'journey.detail.noPhotosHint':
|
||||
'Nahrajte fotky k záznamům nebo procházejte knihovnu Immich/Synology',
|
||||
'journey.detail.noPhotosHint': 'Nahrajte fotky k záznamům nebo procházejte knihovnu Immich/Synology',
|
||||
'journey.detail.journeyStats': 'Statistiky cesty',
|
||||
'journey.detail.syncedTrips': 'Synchronizované cesty',
|
||||
'journey.detail.noTripsLinked': 'Zatím žádné propojené cesty',
|
||||
@@ -115,8 +106,7 @@ const journey: TranslationStrings = {
|
||||
'journey.editor.uploadPhotos': 'Nahrát fotky',
|
||||
'journey.editor.uploading': 'Nahrávání...',
|
||||
'journey.editor.uploadingProgress': 'Nahrávání {done}/{total}…',
|
||||
'journey.editor.uploadPartialFailed':
|
||||
'{failed} z {total} fotek selhalo — uložte znovu pro opakování',
|
||||
'journey.editor.uploadPartialFailed': '{failed} z {total} fotek selhalo — uložte znovu pro opakování',
|
||||
'journey.editor.fromGallery': 'Z galerie',
|
||||
'journey.editor.allPhotosAdded': 'Všechny fotky již přidány',
|
||||
'journey.editor.writeStory': 'Napište svůj příběh...',
|
||||
@@ -195,12 +185,10 @@ const journey: TranslationStrings = {
|
||||
'journey.settings.reopenJourney': 'Obnovit cestu',
|
||||
'journey.settings.archived': 'Cesta archivována',
|
||||
'journey.settings.reopened': 'Cesta znovu otevřena',
|
||||
'journey.settings.endDescription':
|
||||
'Skryje odznak Živě. Kdykoli jej lze znovu otevřít.',
|
||||
'journey.settings.endDescription': 'Skryje odznak Živě. Kdykoli jej lze znovu otevřít.',
|
||||
'journey.settings.delete': 'Smazat',
|
||||
'journey.settings.deleteJourney': 'Smazat cestovní deník',
|
||||
'journey.settings.deleteMessage':
|
||||
'Smazat „{title}"? Všechny záznamy a fotky budou ztraceny.',
|
||||
'journey.settings.deleteMessage': 'Smazat „{title}"? Všechny záznamy a fotky budou ztraceny.',
|
||||
'journey.settings.saved': 'Nastavení uloženo',
|
||||
'journey.settings.saveFailed': 'Uložení selhalo',
|
||||
'journey.settings.coverUpdated': 'Obal aktualizován',
|
||||
@@ -211,8 +199,7 @@ const journey: TranslationStrings = {
|
||||
'journey.photosUploadFailed': 'Některé fotky se nepodařilo nahrát',
|
||||
'journey.photosAdded': '{count} fotografií přidáno',
|
||||
'journey.public.notFound': 'Nenalezeno',
|
||||
'journey.public.notFoundMessage':
|
||||
'Tento cestovní deník neexistuje nebo odkaz vypršel.',
|
||||
'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.',
|
||||
'journey.public.readOnly': 'Pouze ke čtení · Veřejný cestovní deník',
|
||||
'journey.public.tagline': 'Travel Resource & Exploration Kit',
|
||||
'journey.public.sharedVia': 'Sdíleno přes',
|
||||
|
||||
+11
-22
@@ -3,8 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const login: TranslationStrings = {
|
||||
'login.error': 'Přihlášení se nezdařilo. Zkontrolujte prosím své údaje.',
|
||||
'login.tagline': 'Vaše cesty.\nVáš plán.',
|
||||
'login.description':
|
||||
'Plánujte cesty společně s interaktivními mapami, rozpočty a synchronizací v reálném čase.',
|
||||
'login.description': '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.mapsDesc': 'Google Places, trasy a shlukování bodů',
|
||||
'login.features.realtime': 'Synchronizace v reálném čase',
|
||||
@@ -20,8 +19,7 @@ const login: TranslationStrings = {
|
||||
'login.features.files': 'Dokumenty',
|
||||
'login.features.filesDesc': 'Nahrávejte a spravujte dokumenty',
|
||||
'login.features.routes': 'Chytré trasy',
|
||||
'login.features.routesDesc':
|
||||
'Automatická optimalizace a export do Google Maps',
|
||||
'login.features.routesDesc': 'Automatická optimalizace a export do Google Maps',
|
||||
'login.selfHosted': 'Self-hosted · Open Source · Vaše data zůstávají u vás',
|
||||
'login.title': 'Přihlásit se',
|
||||
'login.subtitle': 'Vítejte zpět',
|
||||
@@ -39,24 +37,20 @@ const login: TranslationStrings = {
|
||||
'login.register': 'Registrovat se',
|
||||
'login.emailPlaceholder': 'vas@email.cz',
|
||||
'login.username': 'Uživatelské jméno',
|
||||
'login.oidc.registrationDisabled':
|
||||
'Registrace je zakázána. Kontaktujte svého administrátora.',
|
||||
'login.oidc.registrationDisabled': 'Registrace je zakázána. Kontaktujte svého administrátora.',
|
||||
'login.oidc.noEmail': 'Od poskytovatele nebyl přijat žádný e-mail.',
|
||||
'login.oidc.tokenFailed': 'Ověření se nezdařilo.',
|
||||
'login.oidc.invalidState': 'Neplatná relace. Zkuste to prosím znovu.',
|
||||
'login.demoFailed': 'Přihlášení do dema se nezdařilo',
|
||||
'login.oidcSignIn': 'Přihlásit se přes {name}',
|
||||
'login.oidcOnly':
|
||||
'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.oidcOnly': '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.demoHint': 'Vyzkoušejte demo – registrace není nutná',
|
||||
'login.mfaTitle': 'Dvoufaktorové ověření',
|
||||
'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.',
|
||||
'login.mfaCodeLabel': 'Ověřovací kód',
|
||||
'login.mfaCodeRequired': 'Zadejte kód z aplikace.',
|
||||
'login.mfaHint':
|
||||
'Otevřete Google Authenticator, Authy nebo jinou TOTP aplikaci.',
|
||||
'login.mfaHint': 'Otevřete Google Authenticator, Authy nebo jinou TOTP aplikaci.',
|
||||
'login.mfaBack': '← Zpět k přihlášení',
|
||||
'login.mfaVerify': 'Ověřit',
|
||||
'login.invalidInviteLink': 'Neplatný nebo vypršelý odkaz s pozvánkou',
|
||||
@@ -66,8 +60,7 @@ const login: TranslationStrings = {
|
||||
'login.forgotPassword': 'Zapomenuté heslo?',
|
||||
'login.rememberMe': 'Zapamatovat si mě',
|
||||
'login.forgotPasswordTitle': 'Obnovení hesla',
|
||||
'login.forgotPasswordBody':
|
||||
'Zadej e-mail použitý při registraci. Pokud účet existuje, pošleme odkaz pro obnovení.',
|
||||
'login.forgotPasswordBody': 'Zadej e-mail použitý při registraci. Pokud účet existuje, pošleme odkaz pro obnovení.',
|
||||
'login.forgotPasswordSubmit': 'Odeslat odkaz',
|
||||
'login.forgotPasswordSentTitle': 'Zkontroluj e-mail',
|
||||
'login.forgotPasswordSentBody':
|
||||
@@ -80,20 +73,16 @@ const login: TranslationStrings = {
|
||||
'login.passwordsDontMatch': 'Hesla se neshodují',
|
||||
'login.mfaCode': 'Kód 2FA',
|
||||
'login.resetPasswordTitle': 'Nastavit nové heslo',
|
||||
'login.resetPasswordBody':
|
||||
'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.resetPasswordBody': '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.resetPasswordSubmit': 'Obnovit heslo',
|
||||
'login.resetPasswordVerify': 'Ověřit a obnovit',
|
||||
'login.resetPasswordSuccessTitle': 'Heslo aktualizováno',
|
||||
'login.resetPasswordSuccessBody': 'Nyní se můžeš přihlásit novým heslem.',
|
||||
'login.resetPasswordInvalidLink': 'Neplatný odkaz',
|
||||
'login.resetPasswordInvalidLinkBody':
|
||||
'Odkaz chybí nebo je poškozený. Pro pokračování si vyžádej nový.',
|
||||
'login.resetPasswordInvalidLinkBody': '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.passkey.signIn': 'Přihlásit se pomocí přístupového klíče',
|
||||
'login.passkey.failed':
|
||||
'Přihlášení přístupovým klíčem se nezdařilo. Zkuste to prosím znovu.',
|
||||
'login.passkey.failed': 'Přihlášení přístupovým klíčem se nezdařilo. Zkuste to prosím znovu.',
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -3,14 +3,12 @@ import type { TranslationStrings } from '../types';
|
||||
const memories: TranslationStrings = {
|
||||
'memories.title': 'Fotky',
|
||||
'memories.notConnected': 'Immich není připojen',
|
||||
'memories.notConnectedHint':
|
||||
'Připojte svoji instanci Immich v Nastavení, abyste zde viděli fotky z cest.',
|
||||
'memories.notConnectedHint': 'Připojte svoji instanci Immich v Nastavení, abyste zde viděli fotky z cest.',
|
||||
'memories.notConnectedMultipleHint':
|
||||
'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.noPhotos': 'Nenalezeny žádné fotky',
|
||||
'memories.noPhotosHint':
|
||||
'V Immich nebyly nalezeny žádné fotky pro období této cesty.',
|
||||
'memories.noPhotosHint': 'V Immich nebyly nalezeny žádné fotky pro období této cesty.',
|
||||
'memories.photosFound': 'fotek',
|
||||
'memories.fromOthers': 'od ostatních',
|
||||
'memories.sharePhotos': 'Sdílet fotky',
|
||||
@@ -24,10 +22,8 @@ const memories: TranslationStrings = {
|
||||
'memories.providerPassword': 'Heslo',
|
||||
'memories.providerOTP': 'MFA kód (pokud je povoleno)',
|
||||
'memories.skipSSLVerification': 'Přeskočit ověření SSL certifikátu',
|
||||
'memories.immichAutoUpload':
|
||||
'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.immichAutoUpload': '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.testConnection': 'Otestovat připojení',
|
||||
'memories.testShort': 'Otestovat',
|
||||
'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.removePhoto': 'Odebrání fotky se nezdařilo',
|
||||
'memories.error.toggleSharing': 'Aktualizace sdílení se nezdařila',
|
||||
'memories.saveRouteNotConfigured':
|
||||
'Trasa uložení není nakonfigurována pro tohoto poskytovatele',
|
||||
'memories.testRouteNotConfigured':
|
||||
'Testovací trasa není nakonfigurována pro tohoto poskytovatele',
|
||||
'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele',
|
||||
'memories.testRouteNotConfigured': 'Testovací trasa není nakonfigurována pro tohoto poskytovatele',
|
||||
'memories.fillRequiredFields': 'Prosím vyplňte všechna povinná pole',
|
||||
};
|
||||
export default memories;
|
||||
|
||||
@@ -14,8 +14,7 @@ const notif: TranslationStrings = {
|
||||
'notif.todo_due.title': 'Úkol se blíží',
|
||||
'notif.todo_due.text': '{todo} ve výletě {trip} má termín {due}',
|
||||
'notif.vacay_invite.title': 'Pozvánka Vacay Fusion',
|
||||
'notif.vacay_invite.text':
|
||||
'{actor} vás pozval ke spojení dovolenkových plánů',
|
||||
'notif.vacay_invite.text': '{actor} vás pozval ke spojení dovolenkových plánů',
|
||||
'notif.photos_shared.title': 'Fotky sdíleny',
|
||||
'notif.photos_shared.text': '{actor} sdílel {count} foto v {trip}',
|
||||
'notif.collab_message.title': 'Nová zpráva',
|
||||
@@ -36,7 +35,6 @@ const notif: TranslationStrings = {
|
||||
'notif.generic.title': 'Oznámení',
|
||||
'notif.generic.text': 'Máte nové oznámení',
|
||||
'notif.dev.unknown_event.title': '[DEV] Neznámá událost',
|
||||
'notif.dev.unknown_event.text':
|
||||
'Typ události "{event}" není registrován v EVENT_NOTIFICATION_CONFIG',
|
||||
'notif.dev.unknown_event.text': 'Typ události "{event}" není registrován v EVENT_NOTIFICATION_CONFIG',
|
||||
};
|
||||
export default notif;
|
||||
|
||||
@@ -26,8 +26,7 @@ const notifications: TranslationStrings = {
|
||||
'notifications.test.navigateText': 'Testovací navigační oznámení.',
|
||||
'notifications.test.goThere': 'Přejít tam',
|
||||
'notifications.test.adminTitle': 'Hromadná zpráva pro správce',
|
||||
'notifications.test.adminText':
|
||||
'{actor} odeslal testovací oznámení všem správcům.',
|
||||
'notifications.test.adminText': '{actor} odeslal testovací oznámení všem správcům.',
|
||||
'notifications.test.tripTitle': '{actor} přispěl do vašeho výletu',
|
||||
'notifications.test.tripText': 'Testovací oznámení pro výlet "{trip}".',
|
||||
'notifications.versionAvailable.title': 'Dostupná aktualizace',
|
||||
|
||||
+31
-62
@@ -17,95 +17,66 @@ const oauth: TranslationStrings = {
|
||||
'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:write.label': 'Upravit výlety a itineráře',
|
||||
'oauth.scope.trips:write.description':
|
||||
'Vytvářet a aktualizovat výlety, dny, poznámky a spravovat členy',
|
||||
'oauth.scope.trips:write.description': 'Vytvářet a aktualizovat výlety, dny, poznámky a spravovat členy',
|
||||
'oauth.scope.trips:delete.label': 'Mazat výlety',
|
||||
'oauth.scope.trips:delete.description':
|
||||
'Trvale smazat celé výlety — tato akce je nevratná',
|
||||
'oauth.scope.trips:delete.description': 'Trvale smazat celé výlety — tato akce je nevratná',
|
||||
'oauth.scope.trips:share.label': 'Spravovat sdílené odkazy',
|
||||
'oauth.scope.trips:share.description':
|
||||
'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy',
|
||||
'oauth.scope.trips:share.description': '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.description':
|
||||
'Číst místa, denní přiřazení, štítky a kategorie',
|
||||
'oauth.scope.places:read.description': 'Číst místa, denní přiřazení, štítky a kategorie',
|
||||
'oauth.scope.places:write.label': 'Spravovat místa',
|
||||
'oauth.scope.places:write.description':
|
||||
'Vytvářet, aktualizovat a mazat místa, přiřazení a štítky',
|
||||
'oauth.scope.places:write.description': 'Vytvářet, aktualizovat a mazat místa, přiřazení a štítky',
|
||||
'oauth.scope.atlas:read.label': 'Zobrazit Atlas',
|
||||
'oauth.scope.atlas:read.description':
|
||||
'Číst navštívené země, regiony a seznam přání',
|
||||
'oauth.scope.atlas:read.description': 'Číst navštívené země, regiony a seznam přání',
|
||||
'oauth.scope.atlas:write.label': 'Spravovat Atlas',
|
||||
'oauth.scope.atlas:write.description':
|
||||
'Označovat navštívené země a regiony, spravovat seznam přání',
|
||||
'oauth.scope.atlas:write.description': 'Označovat navštívené země a regiony, spravovat seznam přání',
|
||||
'oauth.scope.packing:read.label': 'Zobrazit seznamy balení',
|
||||
'oauth.scope.packing:read.description':
|
||||
'Číst položky, tašky a přiřazení kategorií',
|
||||
'oauth.scope.packing:read.description': 'Číst položky, tašky a přiřazení kategorií',
|
||||
'oauth.scope.packing:write.label': 'Spravovat seznamy balení',
|
||||
'oauth.scope.packing:write.description':
|
||||
'Přidávat, aktualizovat, mazat, označovat a řadit položky a tašky',
|
||||
'oauth.scope.packing:write.description': '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.description':
|
||||
'Číst úkoly výletu a přiřazení kategorií',
|
||||
'oauth.scope.todos:read.description': 'Číst úkoly výletu a přiřazení kategorií',
|
||||
'oauth.scope.todos:write.label': 'Spravovat seznamy úkolů',
|
||||
'oauth.scope.todos:write.description':
|
||||
'Vytvářet, aktualizovat, označovat, mazat a řadit úkoly',
|
||||
'oauth.scope.todos:write.description': 'Vytvářet, aktualizovat, označovat, mazat a řadit úkoly',
|
||||
'oauth.scope.budget:read.label': 'Zobrazit rozpočet',
|
||||
'oauth.scope.budget:read.description':
|
||||
'Číst položky rozpočtu a přehled výdajů',
|
||||
'oauth.scope.budget:read.description': 'Číst položky rozpočtu a přehled výdajů',
|
||||
'oauth.scope.budget:write.label': 'Spravovat rozpočet',
|
||||
'oauth.scope.budget:write.description':
|
||||
'Vytvářet, aktualizovat a mazat položky rozpočtu',
|
||||
'oauth.scope.budget:write.description': 'Vytvářet, aktualizovat a mazat položky rozpočtu',
|
||||
'oauth.scope.reservations:read.label': 'Zobrazit rezervace',
|
||||
'oauth.scope.reservations:read.description':
|
||||
'Číst rezervace a podrobnosti ubytování',
|
||||
'oauth.scope.reservations:read.description': 'Číst rezervace a podrobnosti ubytování',
|
||||
'oauth.scope.reservations:write.label': 'Spravovat rezervace',
|
||||
'oauth.scope.reservations:write.description':
|
||||
'Vytvářet, aktualizovat, mazat a řadit rezervace',
|
||||
'oauth.scope.reservations:write.description': 'Vytvářet, aktualizovat, mazat a řadit rezervace',
|
||||
'oauth.scope.collab:read.label': 'Zobrazit spolupráci',
|
||||
'oauth.scope.collab:read.description':
|
||||
'Číst poznámky, ankety a zprávy spolupráce',
|
||||
'oauth.scope.collab:read.description': 'Číst poznámky, ankety a zprávy spolupráce',
|
||||
'oauth.scope.collab:write.label': 'Spravovat spolupráci',
|
||||
'oauth.scope.collab:write.description':
|
||||
'Vytvářet, aktualizovat a mazat poznámky, ankety a zprávy',
|
||||
'oauth.scope.collab:write.description': 'Vytvářet, aktualizovat a mazat poznámky, ankety a zprávy',
|
||||
'oauth.scope.notifications:read.label': 'Zobrazit oznámení',
|
||||
'oauth.scope.notifications:read.description':
|
||||
'Číst oznámení v aplikaci a počty nepřečtených',
|
||||
'oauth.scope.notifications:read.description': 'Číst oznámení v aplikaci a počty nepřečtených',
|
||||
'oauth.scope.notifications:write.label': 'Spravovat oznámení',
|
||||
'oauth.scope.notifications:write.description':
|
||||
'Označovat oznámení jako přečtená a reagovat na ně',
|
||||
'oauth.scope.notifications:write.description': '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.description':
|
||||
'Číst data plánování dovolené, záznamy a statistiky',
|
||||
'oauth.scope.vacay:read.description': 'Číst data plánování dovolené, záznamy a statistiky',
|
||||
'oauth.scope.vacay:write.label': 'Spravovat plány dovolené',
|
||||
'oauth.scope.vacay:write.description':
|
||||
'Vytvářet a spravovat záznamy dovolené, svátky a týmové plány',
|
||||
'oauth.scope.vacay:write.description': '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.description':
|
||||
'Vyhledávat místa, řešit URL map a zpětně geokódovat souřadnice',
|
||||
'oauth.scope.geo:read.description': '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.description':
|
||||
'Získávat předpovědi počasí pro místa a data výletu',
|
||||
'oauth.scope.weather:read.description': '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.description':
|
||||
'Číst cestovní deníky, záznamy a seznam přispěvatelů',
|
||||
'oauth.scope.journey:read.description': 'Číst cestovní deníky, záznamy a seznam přispěvatelů',
|
||||
'oauth.scope.journey:write.label': 'Spravovat cestovní deníky',
|
||||
'oauth.scope.journey:write.description':
|
||||
'Vytvářet, aktualizovat a mazat cestovní deníky a jejich záznamy',
|
||||
'oauth.scope.journey:write.description': '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.description':
|
||||
'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy na cestovní deníky',
|
||||
'oauth.scope.journey:share.description': 'Vytvářet, aktualizovat a rušit veřejné sdílené odkazy na cestovní deníky',
|
||||
'oauth.authorize.authorizing': 'Authorizing…', // en-fallback
|
||||
'oauth.authorize.loading': 'Loading…', // en-fallback
|
||||
'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback
|
||||
'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback
|
||||
'oauth.authorize.loginDescription':
|
||||
'{client} wants access to your TREK account. Please sign in first.', // en-fallback
|
||||
'oauth.authorize.loginDescription': '{client} wants access to your TREK account. Please sign in first.', // en-fallback
|
||||
'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback
|
||||
'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback
|
||||
'oauth.authorize.requestDescription':
|
||||
'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.requestDescription': '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.selectScope': 'Select at least one scope', // en-fallback
|
||||
'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // 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.permissionsRequested': 'Permissions requested', // en-fallback
|
||||
'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback
|
||||
'oauth.authorize.alwaysTool.listTrips':
|
||||
'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.listTrips': '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
|
||||
};
|
||||
export default oauth;
|
||||
|
||||
@@ -51,10 +51,8 @@ const packing: TranslationStrings = {
|
||||
'packing.bagName': 'Název zavazadla...',
|
||||
'packing.addBag': 'Přidat zavazadlo',
|
||||
'packing.changeCategory': 'Změnit kategorii',
|
||||
'packing.confirm.clearChecked':
|
||||
'Opravdu chcete odstranit {count} zabalených položek?',
|
||||
'packing.confirm.deleteCat':
|
||||
'Opravdu chcete smazat kategorii „{name}" s {count} položkami?',
|
||||
'packing.confirm.clearChecked': 'Opravdu chcete odstranit {count} zabalených položek?',
|
||||
'packing.confirm.deleteCat': 'Opravdu chcete smazat kategorii „{name}" s {count} položkami?',
|
||||
'packing.defaultCategory': 'Ostatní',
|
||||
'packing.toast.saveError': 'Uložení se nezdařilo',
|
||||
'packing.toast.deleteError': 'Smazání se nezdařilo',
|
||||
|
||||
@@ -32,28 +32,20 @@ const perm: TranslationStrings = {
|
||||
'perm.action.collab_edit': 'Spolupráce (poznámky, hlasování, chat)',
|
||||
'perm.action.share_manage': 'Spravovat odkazy ke sdílení',
|
||||
'perm.actionHint.trip_create': 'Kdo může vytvářet nové výlety',
|
||||
'perm.actionHint.trip_edit':
|
||||
'Kdo může měnit název, data, popis a měnu výletu',
|
||||
'perm.actionHint.trip_edit': '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_archive': 'Kdo může archivovat nebo odarchivovat výlet',
|
||||
'perm.actionHint.trip_cover_upload':
|
||||
'Kdo může nahrát nebo změnit titulní obrázek',
|
||||
'perm.actionHint.trip_cover_upload': '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.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_delete':
|
||||
'Kdo může přesunout soubory do koše nebo je trvale smazat',
|
||||
'perm.actionHint.file_delete': '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.day_edit':
|
||||
'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.budget_edit':
|
||||
'Kdo může vytvářet, upravovat nebo mazat položky rozpočtu',
|
||||
'perm.actionHint.day_edit': '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.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.collab_edit':
|
||||
'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.collab_edit': '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í',
|
||||
};
|
||||
export default perm;
|
||||
|
||||
@@ -6,13 +6,10 @@ const places: TranslationStrings = {
|
||||
'places.sidebarDrop': 'Pusťte pro import',
|
||||
'places.importFileHint':
|
||||
'Importujte soubory .gpx, .kml nebo .kmz z nástrojů jako Google My Maps, Google Earth nebo GPS tracker.',
|
||||
'places.importFileDropHere':
|
||||
'Klikněte pro výběr souboru nebo jej přetáhněte sem',
|
||||
'places.importFileDropHere': '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.importFileUnsupported':
|
||||
'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.importFileUnsupported': '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.importFileError': 'Import se nezdařil',
|
||||
'places.importAllSkipped': 'Všechna místa již byla v cestě.',
|
||||
'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.urlResolved': 'Místo importováno z URL',
|
||||
'places.importList': 'Import seznamu',
|
||||
'places.kmlKmzSummaryValues':
|
||||
'Placemarks: {total} • Importováno: {created} • Přeskočeno: {skipped}',
|
||||
'places.kmlKmzSummaryValues': 'Placemarks: {total} • Importováno: {created} • Přeskočeno: {skipped}',
|
||||
'places.importGoogleList': 'Google Seznam',
|
||||
'places.importNaverList': 'Naver Seznam',
|
||||
'places.googleListHint':
|
||||
'Vložte sdílený odkaz na seznam Google Maps pro import všech míst.',
|
||||
'places.googleListHint': '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.googleListError': 'Import seznamu Google Maps se nezdařil',
|
||||
'places.naverListHint':
|
||||
'Vložte sdílený odkaz na seznam Naver Maps pro import všech míst.',
|
||||
'places.naverListHint': '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.naverListError': 'Import seznamu Naver Maps se nezdařil',
|
||||
'places.viewDetails': 'Zobrazit detaily',
|
||||
@@ -76,8 +70,7 @@ const places: TranslationStrings = {
|
||||
'places.formNotes': 'Poznámky',
|
||||
'places.formNotesPlaceholder': 'Osobní poznámky...',
|
||||
'places.formReservation': 'Rezervace',
|
||||
'places.reservationNotesPlaceholder':
|
||||
'Poznámky k rezervaci, potvrzovací kód...',
|
||||
'places.reservationNotesPlaceholder': 'Poznámky k rezervaci, potvrzovací kód...',
|
||||
'places.mapsSearchPlaceholder': 'Hledat místa...',
|
||||
'places.mapsSearchError': 'Hledání místa se nezdařilo.',
|
||||
'places.loadingDetails': 'Načítání podrobností místa…',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user