From a7adef535333f556de2d9933a6eb83eb80b07b15 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 13 Nov 2026 18:31:07 +0100 Subject: [PATCH] feat(mail-ingest): admin card, review mark, inbox events and links, translations - Admin settings (desktop and phone, self-hosted only): the instance switch, how mail arrives (IMAP or a signed webhook) with its fields, the address template, the AI allowance per person and day, a connection test and the status in counts. Changed fields only are saved; secrets stay masked. - The review mark of an imported booking takes its mark off: the pill in the desktop dialog and a row in the phone's transport sheet ('Looks right'), through the ordinary booking update. Its hint no longer speaks of airports. - The inbox follows mailInbox:changed and mailInbox:settings, and ?inbox=mail (the 'bookings waiting' notification) opens it. - mailIngestRepo: the online-only path the views take, answers parsed against the shared contracts. - Dropped 'read with AI' and the unused mail-review contract: the server reads a passing AI failure again on its own. - mailInbox, mailAdmin and the new reservation, settings and notification strings in all 28 locales; the pending notice is a plural group. - The live flight line on the booking cards is one of the card's fields, full width on the phone; the phone's live card keeps its map to the middle. - Admin flight and mail cards sit in the left column. --- client/scripts/feature-pairs.json | 7 + client/src/api/mailIngest.ts | 13 +- client/src/api/wsEventPolicy.ts | 6 +- .../Admin/mailIngest/MailIngestAdminCard.tsx | 304 ++++++++++++++++++ .../mailIngest/mailIngestAdminModel.test.ts | 99 ++++++ .../Admin/mailIngest/mailIngestAdminModel.ts | 157 +++++++++ .../Admin/mailIngest/useMailIngestAdmin.ts | 124 +++++++ client/src/components/Layout/Navbar.tsx | 12 +- .../components/MailIngest/MailInboxDialog.tsx | 8 +- .../components/MailIngest/TripMailForward.tsx | 18 +- .../MailIngest/mailInboxModel.test.ts | 9 +- .../components/MailIngest/mailInboxModel.ts | 134 +++----- .../MailIngest/useMailImportSettings.ts | 13 +- .../src/components/MailIngest/useMailInbox.ts | 47 ++- .../components/MailIngest/useMailInboxLink.ts | 30 ++ .../MailIngest/useMailPrefillLink.ts | 22 +- .../Notifications/notificationModel.ts | 3 + .../Planner/bookings/BookingDetailDialog.tsx | 11 +- .../Planner/bookings/bookingParts.test.tsx | 13 +- .../Planner/bookings/bookingParts.tsx | 26 +- .../Planner/flightLive/FlightLiveStrip.tsx | 41 +-- .../Planner/flightLive/flightMiniMap.css | 11 + .../components/Settings/notificationLabels.ts | 3 + .../screens/admin/MAdminSettingsSection.tsx | 4 + .../screens/admin/MMailIngestAdminCard.tsx | 215 +++++++++++++ .../mobile/screens/mail/MMailInboxHost.tsx | 14 +- .../mobile/screens/mail/MMailInboxSheet.tsx | 10 +- .../mobile/screens/trip/MFlightLiveStrip.tsx | 51 ++- .../screens/trip/sheets/MFlightLiveCard.tsx | 11 +- .../screens/trip/sheets/MReviewNotice.tsx | 32 ++ .../screens/trip/sheets/MTransportSheet.tsx | 12 + .../screens/trip/tabs/MTransportsTab.tsx | 2 +- client/src/pages/admin/AdminSettingsTab.tsx | 41 +-- client/src/repo/mailIngestRepo.test.ts | 38 +++ client/src/repo/mailIngestRepo.ts | 104 ++++++ client/src/store/mailIngestStore.test.ts | 31 +- client/src/store/mailIngestStore.ts | 56 ++-- client/tests/helpers/storeRegistry.ts | 3 + shared/src/i18n/ar/index.ts | 4 + shared/src/i18n/ar/mailAdmin.ts | 45 +++ shared/src/i18n/ar/mailInbox.ts | 100 ++++++ shared/src/i18n/ar/notif.ts | 7 +- shared/src/i18n/ar/reservations.ts | 5 +- shared/src/i18n/ar/settings.ts | 3 + shared/src/i18n/az/index.ts | 4 + shared/src/i18n/az/mailAdmin.ts | 47 +++ shared/src/i18n/az/mailInbox.ts | 99 ++++++ shared/src/i18n/az/notif.ts | 3 +- shared/src/i18n/az/reservations.ts | 5 +- shared/src/i18n/az/settings.ts | 3 + shared/src/i18n/br/index.ts | 4 + shared/src/i18n/br/mailAdmin.ts | 47 +++ shared/src/i18n/br/mailInbox.ts | 99 ++++++ shared/src/i18n/br/notif.ts | 3 +- shared/src/i18n/br/reservations.ts | 6 +- shared/src/i18n/br/settings.ts | 3 + shared/src/i18n/ca/index.ts | 4 + shared/src/i18n/ca/mailAdmin.ts | 47 +++ shared/src/i18n/ca/mailInbox.ts | 99 ++++++ shared/src/i18n/ca/notif.ts | 3 +- shared/src/i18n/ca/reservations.ts | 5 +- shared/src/i18n/ca/settings.ts | 3 + shared/src/i18n/cs/index.ts | 4 + shared/src/i18n/cs/mailAdmin.ts | 47 +++ shared/src/i18n/cs/mailInbox.ts | 98 ++++++ shared/src/i18n/cs/notif.ts | 4 +- shared/src/i18n/cs/reservations.ts | 6 +- shared/src/i18n/cs/settings.ts | 3 + shared/src/i18n/de/index.ts | 4 + shared/src/i18n/de/mailAdmin.ts | 47 +++ shared/src/i18n/de/mailInbox.ts | 99 ++++++ shared/src/i18n/de/notif.ts | 3 +- shared/src/i18n/de/reservations.ts | 6 +- shared/src/i18n/de/settings.ts | 3 + shared/src/i18n/en/index.ts | 4 + shared/src/i18n/en/mailAdmin.ts | 46 +++ shared/src/i18n/en/mailInbox.ts | 97 ++++++ shared/src/i18n/en/notif.ts | 3 +- shared/src/i18n/en/reservations.ts | 6 +- shared/src/i18n/en/settings.ts | 3 + shared/src/i18n/es/index.ts | 4 + shared/src/i18n/es/mailAdmin.ts | 47 +++ shared/src/i18n/es/mailInbox.ts | 99 ++++++ shared/src/i18n/es/notif.ts | 3 +- shared/src/i18n/es/reservations.ts | 5 +- shared/src/i18n/es/settings.ts | 3 + shared/src/i18n/et/index.ts | 4 + shared/src/i18n/et/mailAdmin.ts | 47 +++ shared/src/i18n/et/mailInbox.ts | 99 ++++++ shared/src/i18n/et/notif.ts | 3 +- shared/src/i18n/et/reservations.ts | 6 +- shared/src/i18n/et/settings.ts | 3 + shared/src/i18n/fr/index.ts | 4 + shared/src/i18n/fr/mailAdmin.ts | 47 +++ shared/src/i18n/fr/mailInbox.ts | 100 ++++++ shared/src/i18n/fr/notif.ts | 3 +- shared/src/i18n/fr/reservations.ts | 5 +- shared/src/i18n/fr/settings.ts | 3 + shared/src/i18n/gr/index.ts | 4 + shared/src/i18n/gr/mailAdmin.ts | 47 +++ shared/src/i18n/gr/mailInbox.ts | 100 ++++++ shared/src/i18n/gr/notif.ts | 3 +- shared/src/i18n/gr/reservations.ts | 5 +- shared/src/i18n/gr/settings.ts | 3 + shared/src/i18n/he/index.ts | 4 + shared/src/i18n/he/mailAdmin.ts | 45 +++ shared/src/i18n/he/mailInbox.ts | 97 ++++++ shared/src/i18n/he/notif.ts | 4 +- shared/src/i18n/he/reservations.ts | 5 +- shared/src/i18n/he/settings.ts | 3 + shared/src/i18n/hu/index.ts | 4 + shared/src/i18n/hu/mailAdmin.ts | 47 +++ shared/src/i18n/hu/mailInbox.ts | 99 ++++++ shared/src/i18n/hu/notif.ts | 3 +- shared/src/i18n/hu/reservations.ts | 6 +- shared/src/i18n/hu/settings.ts | 3 + shared/src/i18n/id/index.ts | 4 + shared/src/i18n/id/mailAdmin.ts | 47 +++ shared/src/i18n/id/mailInbox.ts | 98 ++++++ shared/src/i18n/id/notif.ts | 2 +- shared/src/i18n/id/reservations.ts | 6 +- shared/src/i18n/id/settings.ts | 3 + shared/src/i18n/it/index.ts | 4 + shared/src/i18n/it/mailAdmin.ts | 48 +++ shared/src/i18n/it/mailInbox.ts | 101 ++++++ shared/src/i18n/it/notif.ts | 3 +- shared/src/i18n/it/reservations.ts | 6 +- shared/src/i18n/it/settings.ts | 3 + shared/src/i18n/ja/index.ts | 4 + shared/src/i18n/ja/mailAdmin.ts | 47 +++ shared/src/i18n/ja/mailInbox.ts | 97 ++++++ shared/src/i18n/ja/notif.ts | 2 +- shared/src/i18n/ja/reservations.ts | 6 +- shared/src/i18n/ja/settings.ts | 3 + shared/src/i18n/ko/index.ts | 4 + shared/src/i18n/ko/mailAdmin.ts | 45 +++ shared/src/i18n/ko/mailInbox.ts | 98 ++++++ shared/src/i18n/ko/notif.ts | 2 +- shared/src/i18n/ko/reservations.ts | 5 +- shared/src/i18n/ko/settings.ts | 3 + shared/src/i18n/nl/index.ts | 4 + shared/src/i18n/nl/mailAdmin.ts | 47 +++ shared/src/i18n/nl/mailInbox.ts | 98 ++++++ shared/src/i18n/nl/notif.ts | 3 +- shared/src/i18n/nl/reservations.ts | 6 +- shared/src/i18n/nl/settings.ts | 3 + shared/src/i18n/pl/index.ts | 4 + shared/src/i18n/pl/mailAdmin.ts | 47 +++ shared/src/i18n/pl/mailInbox.ts | 101 ++++++ shared/src/i18n/pl/notif.ts | 5 +- shared/src/i18n/pl/reservations.ts | 6 +- shared/src/i18n/pl/settings.ts | 3 + shared/src/i18n/ru/index.ts | 4 + shared/src/i18n/ru/mailAdmin.ts | 47 +++ shared/src/i18n/ru/mailInbox.ts | 100 ++++++ shared/src/i18n/ru/notif.ts | 5 +- shared/src/i18n/ru/reservations.ts | 6 +- shared/src/i18n/ru/settings.ts | 3 + shared/src/i18n/sk/index.ts | 4 + shared/src/i18n/sk/mailAdmin.ts | 47 +++ shared/src/i18n/sk/mailInbox.ts | 100 ++++++ shared/src/i18n/sk/notif.ts | 4 +- shared/src/i18n/sk/reservations.ts | 6 +- shared/src/i18n/sk/settings.ts | 3 + shared/src/i18n/sv/index.ts | 4 + shared/src/i18n/sv/mailAdmin.ts | 46 +++ shared/src/i18n/sv/mailInbox.ts | 98 ++++++ shared/src/i18n/sv/notif.ts | 3 +- shared/src/i18n/sv/reservations.ts | 6 +- shared/src/i18n/sv/settings.ts | 3 + shared/src/i18n/th/index.ts | 4 + shared/src/i18n/th/mailAdmin.ts | 46 +++ shared/src/i18n/th/mailInbox.ts | 95 ++++++ shared/src/i18n/th/notif.ts | 2 +- shared/src/i18n/th/reservations.ts | 5 +- shared/src/i18n/th/settings.ts | 3 + shared/src/i18n/tr/index.ts | 4 + shared/src/i18n/tr/mailAdmin.ts | 47 +++ shared/src/i18n/tr/mailInbox.ts | 98 ++++++ shared/src/i18n/tr/notif.ts | 3 +- shared/src/i18n/tr/reservations.ts | 5 +- shared/src/i18n/tr/settings.ts | 3 + shared/src/i18n/uk/index.ts | 4 + shared/src/i18n/uk/mailAdmin.ts | 47 +++ shared/src/i18n/uk/mailInbox.ts | 100 ++++++ shared/src/i18n/uk/notif.ts | 5 +- shared/src/i18n/uk/reservations.ts | 6 +- shared/src/i18n/uk/settings.ts | 3 + shared/src/i18n/vi/index.ts | 4 + shared/src/i18n/vi/mailAdmin.ts | 46 +++ shared/src/i18n/vi/mailInbox.ts | 97 ++++++ shared/src/i18n/vi/notif.ts | 2 +- shared/src/i18n/vi/reservations.ts | 6 +- shared/src/i18n/vi/settings.ts | 3 + shared/src/i18n/zh-TW/index.ts | 4 + shared/src/i18n/zh-TW/mailAdmin.ts | 45 +++ shared/src/i18n/zh-TW/mailInbox.ts | 91 ++++++ shared/src/i18n/zh-TW/notif.ts | 2 +- shared/src/i18n/zh-TW/reservations.ts | 5 +- shared/src/i18n/zh-TW/settings.ts | 3 + shared/src/i18n/zh/index.ts | 4 + shared/src/i18n/zh/mailAdmin.ts | 45 +++ shared/src/i18n/zh/mailInbox.ts | 92 ++++++ shared/src/i18n/zh/notif.ts | 2 +- shared/src/i18n/zh/reservations.ts | 5 +- shared/src/i18n/zh/settings.ts | 3 + .../mail-ingest/mail-ingest.schema.spec.ts | 8 - shared/src/mail-ingest/mail-ingest.schema.ts | 16 - wiki/Booking-Mail-Import.md | 5 +- 209 files changed, 5915 insertions(+), 343 deletions(-) create mode 100644 client/src/components/Admin/mailIngest/MailIngestAdminCard.tsx create mode 100644 client/src/components/Admin/mailIngest/mailIngestAdminModel.test.ts create mode 100644 client/src/components/Admin/mailIngest/mailIngestAdminModel.ts create mode 100644 client/src/components/Admin/mailIngest/useMailIngestAdmin.ts create mode 100644 client/src/components/MailIngest/useMailInboxLink.ts create mode 100644 client/src/mobile/screens/admin/MMailIngestAdminCard.tsx create mode 100644 client/src/mobile/screens/trip/sheets/MReviewNotice.tsx create mode 100644 client/src/repo/mailIngestRepo.test.ts create mode 100644 client/src/repo/mailIngestRepo.ts create mode 100644 shared/src/i18n/ar/mailAdmin.ts create mode 100644 shared/src/i18n/ar/mailInbox.ts create mode 100644 shared/src/i18n/az/mailAdmin.ts create mode 100644 shared/src/i18n/az/mailInbox.ts create mode 100644 shared/src/i18n/br/mailAdmin.ts create mode 100644 shared/src/i18n/br/mailInbox.ts create mode 100644 shared/src/i18n/ca/mailAdmin.ts create mode 100644 shared/src/i18n/ca/mailInbox.ts create mode 100644 shared/src/i18n/cs/mailAdmin.ts create mode 100644 shared/src/i18n/cs/mailInbox.ts create mode 100644 shared/src/i18n/de/mailAdmin.ts create mode 100644 shared/src/i18n/de/mailInbox.ts create mode 100644 shared/src/i18n/en/mailAdmin.ts create mode 100644 shared/src/i18n/en/mailInbox.ts create mode 100644 shared/src/i18n/es/mailAdmin.ts create mode 100644 shared/src/i18n/es/mailInbox.ts create mode 100644 shared/src/i18n/et/mailAdmin.ts create mode 100644 shared/src/i18n/et/mailInbox.ts create mode 100644 shared/src/i18n/fr/mailAdmin.ts create mode 100644 shared/src/i18n/fr/mailInbox.ts create mode 100644 shared/src/i18n/gr/mailAdmin.ts create mode 100644 shared/src/i18n/gr/mailInbox.ts create mode 100644 shared/src/i18n/he/mailAdmin.ts create mode 100644 shared/src/i18n/he/mailInbox.ts create mode 100644 shared/src/i18n/hu/mailAdmin.ts create mode 100644 shared/src/i18n/hu/mailInbox.ts create mode 100644 shared/src/i18n/id/mailAdmin.ts create mode 100644 shared/src/i18n/id/mailInbox.ts create mode 100644 shared/src/i18n/it/mailAdmin.ts create mode 100644 shared/src/i18n/it/mailInbox.ts create mode 100644 shared/src/i18n/ja/mailAdmin.ts create mode 100644 shared/src/i18n/ja/mailInbox.ts create mode 100644 shared/src/i18n/ko/mailAdmin.ts create mode 100644 shared/src/i18n/ko/mailInbox.ts create mode 100644 shared/src/i18n/nl/mailAdmin.ts create mode 100644 shared/src/i18n/nl/mailInbox.ts create mode 100644 shared/src/i18n/pl/mailAdmin.ts create mode 100644 shared/src/i18n/pl/mailInbox.ts create mode 100644 shared/src/i18n/ru/mailAdmin.ts create mode 100644 shared/src/i18n/ru/mailInbox.ts create mode 100644 shared/src/i18n/sk/mailAdmin.ts create mode 100644 shared/src/i18n/sk/mailInbox.ts create mode 100644 shared/src/i18n/sv/mailAdmin.ts create mode 100644 shared/src/i18n/sv/mailInbox.ts create mode 100644 shared/src/i18n/th/mailAdmin.ts create mode 100644 shared/src/i18n/th/mailInbox.ts create mode 100644 shared/src/i18n/tr/mailAdmin.ts create mode 100644 shared/src/i18n/tr/mailInbox.ts create mode 100644 shared/src/i18n/uk/mailAdmin.ts create mode 100644 shared/src/i18n/uk/mailInbox.ts create mode 100644 shared/src/i18n/vi/mailAdmin.ts create mode 100644 shared/src/i18n/vi/mailInbox.ts create mode 100644 shared/src/i18n/zh-TW/mailAdmin.ts create mode 100644 shared/src/i18n/zh-TW/mailInbox.ts create mode 100644 shared/src/i18n/zh/mailAdmin.ts create mode 100644 shared/src/i18n/zh/mailInbox.ts diff --git a/client/scripts/feature-pairs.json b/client/scripts/feature-pairs.json index d3ebaa3e5..25d1e3c21 100644 --- a/client/scripts/feature-pairs.json +++ b/client/scripts/feature-pairs.json @@ -142,6 +142,12 @@ "desktop": ["src/components/Settings/MailImportSection.tsx"], "mobile": ["src/mobile/screens/settings/MMailImportSection.tsx"] }, + { + "feature": "Booking mail import: admin card", + "hooks": ["src/components/Admin/mailIngest/useMailIngestAdmin.ts", "src/components/Admin/mailIngest/mailIngestAdminModel.ts"], + "desktop": ["src/components/Admin/mailIngest/MailIngestAdminCard.tsx"], + "mobile": ["src/mobile/screens/admin/MMailIngestAdminCard.tsx"] + }, { "feature": "Booking save follow-up: travellers and files before the editor closes", "hooks": ["src/components/Planner/useBookingFollowUp.ts"], @@ -1039,6 +1045,7 @@ } ], "mobileOnly": { + "src/mobile/screens/trip/sheets/MReviewNotice.tsx": "The transport sheet's row asking for a look at an imported booking; the desktop dialog folds the same \"looks right\" action into its review pill (ReviewPill in bookingParts.tsx), so there is no separate twin.", "src/mobile/screens/mail/MMailInboxHost.tsx": "Mounts the phone shell's one inbox sheet and reads the person's import settings; the desktop mounts its dialog from the Navbar, so there is no twin.", "src/mobile/screens/admin/MAdminOidcCard.tsx": "Markup only; the OIDC card of MAdminSettingsSection, with the form state from useAdmin and the save from its useAdminSettingsActions handed in through props.", "src/mobile/screens/admin/MAdminResetMfaSheet.tsx": "Markup only; the 2FA reset confirm that MAdminSheets opens, with the reset handed in through props from its useAdminUserActions.", diff --git a/client/src/api/mailIngest.ts b/client/src/api/mailIngest.ts index f38d7fe53..321ba22df 100644 --- a/client/src/api/mailIngest.ts +++ b/client/src/api/mailIngest.ts @@ -9,9 +9,17 @@ import apiClient from './client'; */ const data = (response: { data: unknown }) => response.data; -export type MailIngestAction = 'apply' | 'move' | 'file' | 'undo' | 'discard' | 'read-ai'; +export type MailIngestAction = 'apply' | 'move' | 'file' | 'undo' | 'discard'; export const mailIngestApi = { + // The admin's view: how the import runs, in counts only, and whether the transport reaches its mail now. + adminStatus: (): Promise => apiClient.get('/admin/mail-ingest/status').then(data), + adminTest: (): Promise => apiClient.post('/admin/mail-ingest/test', {}).then(data), + /** The stored transport fields, passwords masked, from the admin's app settings. */ + adminSettings: (): Promise => apiClient.get('/auth/app-settings').then(data), + saveAdminSettings: (patch: Record): Promise => + apiClient.put('/auth/app-settings', patch).then(data), + settings: (): Promise => apiClient.get('/mail-ingest/settings').then(data), updateSettings: (body: { enabled?: boolean; ai_enabled?: boolean; auto_add?: boolean }): Promise => apiClient.patch('/mail-ingest/settings', body).then(data), @@ -32,9 +40,6 @@ export const mailIngestApi = { act: (candidateId: number, action: MailIngestAction, body: Record = {}): Promise => apiClient.post(`/mail-ingest/candidates/${candidateId}/${action}`, body).then(data), - review: (tripId: number, reservationId: number, fields?: string[]): Promise => - apiClient.post(`/trips/${tripId}/reservations/${reservationId}/mail-review`, fields ? { fields } : {}).then(data), - upload: (file: File): Promise => { const form = new FormData(); form.append('file', file); diff --git a/client/src/api/wsEventPolicy.ts b/client/src/api/wsEventPolicy.ts index 4a179e1ba..2095dd56c 100644 --- a/client/src/api/wsEventPolicy.ts +++ b/client/src/api/wsEventPolicy.ts @@ -23,6 +23,9 @@ export const HANDLED_OUTSIDE_TRIP_STORE = [ 'tours:visits', // store/flightLiveStore (watchFlightLive, mounted by useFlightLiveSync) merges live flight states. 'flight:live', + // store/mailIngestStore (watchMailInbox, mounted by Navbar and MMailInboxHost): the open count and the settings card. + 'mailInbox:changed', + 'mailInbox:settings', // Collab — Collab/MCollab components + useTripWebSocket's collabFileSync 'collab:note:created', 'collab:note:updated', @@ -161,9 +164,6 @@ export const HANDLED_OUTSIDE_TRIP_STORE = [ export const IGNORED_WS_EVENTS: Partial> = { 'todo:assignees': 'No screen shows who looks after a todo category (todoApi.getCategoryAssignees has no caller), so there is nothing to refresh.', - // The mail import's inbox and settings card come with its client screens, which move these to their listener. - 'mailInbox:changed': 'No screen shows the mail inbox or its open count yet, so there is nothing to refresh.', - 'mailInbox:settings': 'No screen shows the mail import settings card yet, so there is nothing to read again.', } /** diff --git a/client/src/components/Admin/mailIngest/MailIngestAdminCard.tsx b/client/src/components/Admin/mailIngest/MailIngestAdminCard.tsx new file mode 100644 index 000000000..e2c658957 --- /dev/null +++ b/client/src/components/Admin/mailIngest/MailIngestAdminCard.tsx @@ -0,0 +1,304 @@ +import { CheckCircle, Copy, Loader2, Mail, PlugZap, Save, XCircle } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { useTranslation } from '../../../i18n'; +import type { TranslationFn } from '../../../types'; +import { copyText } from '../../../utils/clipboard'; +import { relativeTime } from '../../../utils/relativeTime'; +import { + SETTINGS_BUTTON, + SETTINGS_BUTTON_PRIMARY, + SettingRow, + SettingRows, + SettingsCard, + SettingsHint, + StatusPill, +} from '../../Settings/settingsKit'; +import ToggleSwitch from '../../Settings/ToggleSwitch'; +import CustomSelect from '../../shared/CustomSelect'; +import { EditorField, GRID_2, INPUT, READONLY_BOX } from '../../shared/dialogParts'; +import { useToast } from '../../shared/Toast'; +import { + TRANSPORTS, + problemText, + statusFacts, + transportLabel, + type MailAdminForm, + type MailAdminProblem, +} from './mailIngestAdminModel'; +import { useMailIngestAdmin, type MailIngestAdmin } from './useMailIngestAdmin'; + +type T = TranslationFn; + +/** The address a mail service posts to, shown so the admin can paste it into the sender's settings. */ +function InboundUrl({ t }: { t: T }) { + const toast = useToast(); + const url = `${window.location.origin}/api/mail-ingest/inbound`; + return ( + +
+ {url} + +
+
+ ); +} + +interface FieldsProps { + admin: MailIngestAdmin; + form: MailAdminForm; + t: T; + error: (p: MailAdminProblem) => ReactNode; +} + +function ImapFields({ admin, form, t, error }: FieldsProps) { + return ( +
+
+ + admin.set('imap_host', e.target.value)} + /> + +
+ + admin.set('imap_port', e.target.value)} + /> + + + admin.set('imap_tls', value === 'starttls' ? 'starttls' : 'tls')} + options={[ + { value: 'tls', label: 'TLS' }, + { value: 'starttls', label: 'STARTTLS' }, + ]} + /> + +
+
+
+ + admin.set('imap_user', e.target.value)} + /> + + + admin.set('imap_pass', e.target.value)} + /> + +
+ + admin.set('imap_mailbox', e.target.value)} + /> + +
+ ); +} + +function WebhookFields({ admin, form, t, error }: FieldsProps) { + return ( +
+ + admin.set('webhook_secret', e.target.value)} + /> + + +
+ ); +} + +/** How the import runs, in counts only: no subject, sender or content of a mail is ever shown here. */ +function StatusBlock({ admin, t, locale }: { admin: MailIngestAdmin; t: T; locale: string }) { + const status = admin.status; + if (!status) return null; + const facts = statusFacts(status); + const lastRead = status.last_ok_at + ? relativeTime(Date.parse(status.last_ok_at), locale) + : t('mailAdmin.status.never'); + return ( +
+
+ + {t('mailAdmin.status.title')} + + + {t('mailAdmin.status.lastRead', { time: lastRead })} + +
+
+ {[ + { label: t('mailAdmin.status.mails'), value: facts.mails }, + { label: t('mailAdmin.status.added'), value: facts.added }, + { label: t('mailAdmin.status.waiting'), value: facts.waiting }, + { label: t('mailAdmin.status.failed'), value: facts.failed }, + ].map((f) => ( +
+
{f.label}
+
{f.value}
+
+ ))} +
+ {status.last_error && {status.last_error}} +
+ ); +} + +function TestResult({ admin, t }: { admin: MailIngestAdmin; t: T }) { + if (!admin.test) return null; + return admin.test.ok ? ( + }> + {t('mailAdmin.test.ok')} + + ) : ( + }> + {admin.test.error ?? t('mailAdmin.test.failed')} + + ); +} + +/** Everything under the switch: how mail arrives, its fields, then save, test and the status. */ +function Setup({ admin, form, t, locale }: { admin: MailIngestAdmin; form: MailAdminForm; t: T; locale: string }) { + const error = (problem: MailAdminProblem) => (admin.problems.includes(problem) ? problemText(problem, t) : undefined); + return ( +
+
+ + admin.set('transport', TRANSPORTS.find((x) => x === value) ?? 'off')} + options={TRANSPORTS.map((x) => ({ value: x, label: transportLabel(x, t) }))} + /> + + + admin.set('ai_per_user_daily', e.target.value)} + /> + +
+ + admin.set('address_template', e.target.value)} + /> + + {form.transport === 'imap' && } + {form.transport === 'webhook' && } + {t('mailAdmin.envHint')} +
+ + + +
+ +
+ ); +} + +/** + * The booking mail import in the admin settings of a self-hosted install: the + * instance switch, how mail arrives (IMAP or a signed webhook), the forwarding + * address template, the AI allowance per person and day, a connection test and + * the status in counts. Not shown on a managed install, where the operator + * runs the TREK Cloud relay. + */ +export default function MailIngestAdminCard() { + const { t, locale } = useTranslation(); + const admin = useMailIngestAdmin(true); + return ( + + + admin.toggle(!admin.enabled)} + /> + } + /> + + {admin.enabled && admin.form && } + + ); +} diff --git a/client/src/components/Admin/mailIngest/mailIngestAdminModel.test.ts b/client/src/components/Admin/mailIngest/mailIngestAdminModel.test.ts new file mode 100644 index 000000000..65b717bba --- /dev/null +++ b/client/src/components/Admin/mailIngest/mailIngestAdminModel.test.ts @@ -0,0 +1,99 @@ +// FE-ADMIN-MAILINGEST-001 to FE-ADMIN-MAILINGEST-006 +import type { MailIngestAdminStatusResponse } from '@trek/shared'; +import { describe, expect, it } from 'vitest'; +import { + MASKED, + TRANSPORTS, + changesOf, + enabledOf, + fieldsOf, + formOf, + problemText, + problemsOf, + statusFacts, + transportLabel, +} from './mailIngestAdminModel'; + +const t = (key: string, params?: Record) => + params ? `${key} ${JSON.stringify(params)}` : key; + +describe('mailIngestAdminModel', () => { + it('FE-ADMIN-MAILINGEST-001: reads the stored settings, and the defaults for what the server left out', () => { + expect(formOf({})).toEqual({ + transport: 'off', + address_template: '', + imap_host: '', + imap_port: '993', + imap_tls: 'tls', + imap_user: '', + imap_pass: '', + imap_mailbox: 'INBOX', + webhook_secret: '', + ai_per_user_daily: '10', + }); + const form = formOf({ + mail_ingest_transport: 'imap', + mail_ingest_imap_port: 143, + mail_ingest_imap_tls: 'starttls', + mail_ingest_imap_pass: MASKED, + }); + expect(form).toMatchObject({ transport: 'imap', imap_port: '143', imap_tls: 'starttls', imap_pass: MASKED }); + // The Cloud relay is the operator's, never a choice in this form. + expect(formOf({ mail_ingest_transport: 'cloud' }).transport).toBe('off'); + expect(enabledOf({ mail_ingest_enabled: 'true' })).toBe(true); + expect(enabledOf({})).toBe(false); + }); + + it('FE-ADMIN-MAILINGEST-002: a save carries only what changed, trimmed, under the stored keys', () => { + const saved = formOf({ mail_ingest_imap_pass: MASKED }); + expect(changesOf(saved, saved)).toEqual({}); + const edited = { ...saved, transport: 'imap' as const, imap_host: ' imap.example.com ' }; + expect(changesOf(edited, saved)).toEqual({ + mail_ingest_transport: 'imap', + mail_ingest_imap_host: 'imap.example.com', + }); + }); + + it('FE-ADMIN-MAILINGEST-003: each transport shows only its own fields', () => { + expect(fieldsOf('off')).toEqual([]); + expect(fieldsOf('webhook')).toEqual(['webhook_secret']); + expect(fieldsOf('imap')).toContain('imap_pass'); + }); + + it('FE-ADMIN-MAILINGEST-004: what stops a save is found before the server answers', () => { + const base = formOf({}); + expect(problemsOf(base)).toEqual([]); + expect(problemsOf({ ...base, transport: 'webhook', webhook_secret: 'short' })).toEqual(['secretShort']); + // A stored secret comes back masked, and an empty one switches the webhook off: neither is a problem. + expect(problemsOf({ ...base, transport: 'webhook', webhook_secret: MASKED })).toEqual([]); + expect(problemsOf({ ...base, transport: 'webhook', webhook_secret: '' })).toEqual([]); + expect(problemsOf({ ...base, transport: 'imap', imap_port: '70000' })).toEqual(['portInvalid']); + expect(problemsOf({ ...base, address_template: 'trek@example.com' })).toEqual(['templateToken']); + expect(problemsOf({ ...base, ai_per_user_daily: '2.5' })).toEqual(['dailyInvalid']); + }); + + it('FE-ADMIN-MAILINGEST-005: the status card adds up the last day in counts', () => { + const status: MailIngestAdminStatusResponse = { + transport: 'imap', + last_ok_at: null, + last_error: null, + lease_until: null, + last_day: { by_status: { processed: 7, failed: 2, queued: 1 }, by_reason: {} }, + auto_applied: 5, + undone: 0, + ai_calls_today: 3, + backlog: [2, 4], + }; + expect(statusFacts(status)).toEqual({ mails: 10, added: 5, waiting: 6, failed: 2 }); + }); + + it('FE-ADMIN-MAILINGEST-006: every transport and problem has its words, {token} kept literal', () => { + expect(TRANSPORTS.map((x) => transportLabel(x, t))).toEqual([ + 'mailAdmin.transport.off', + 'mailAdmin.transport.imap', + 'mailAdmin.transport.webhook', + ]); + expect(problemText('templateToken', t)).toContain('"token":"{token}"'); + expect(problemText('secretShort', t)).toBe('mailAdmin.problem.secretShort'); + }); +}); diff --git a/client/src/components/Admin/mailIngest/mailIngestAdminModel.ts b/client/src/components/Admin/mailIngest/mailIngestAdminModel.ts new file mode 100644 index 000000000..e661953d2 --- /dev/null +++ b/client/src/components/Admin/mailIngest/mailIngestAdminModel.ts @@ -0,0 +1,157 @@ +import type { MailIngestAdminStatusResponse, MailIngestImapTls } from '@trek/shared'; +import type { TranslationFn } from '../../../types'; + +/** + * The admin form of the booking mail import, as plain data: which stored app + * setting each field is, how the server's answer becomes the form, which + * fields changed, and what the status card says. Shared by the desktop card + * and the phone's admin section; no React here. + */ + +/** The transports an admin picks on a self-hosted install; the TREK Cloud relay is set by the operator. */ +type MailAdminTransport = 'off' | 'imap' | 'webhook'; + +export interface MailAdminForm { + transport: MailAdminTransport; + address_template: string; + imap_host: string; + imap_port: string; + imap_tls: MailIngestImapTls; + imap_user: string; + /** "••••••••" while a stored password is untouched; the server keeps it then. */ + imap_pass: string; + imap_mailbox: string; + /** Masked like the password once stored. */ + webhook_secret: string; + ai_per_user_daily: string; +} + +export type MailAdminField = keyof MailAdminForm; + +/** What the server sends instead of a stored secret, and takes back as "unchanged". */ +export const MASKED = '••••••••'; + +/** The secret the webhook signs with: empty switches it off, else at least this long. */ +const WEBHOOK_SECRET_MIN = 32; + +const KEY: Record = { + transport: 'mail_ingest_transport', + address_template: 'mail_ingest_address_template', + imap_host: 'mail_ingest_imap_host', + imap_port: 'mail_ingest_imap_port', + imap_tls: 'mail_ingest_imap_tls', + imap_user: 'mail_ingest_imap_user', + imap_pass: 'mail_ingest_imap_pass', + imap_mailbox: 'mail_ingest_imap_mailbox', + webhook_secret: 'mail_ingest_webhook_secret', + ai_per_user_daily: 'mail_ingest_ai_per_user_daily', +}; + +export const ENABLED_KEY = 'mail_ingest_enabled'; + +const str = (value: unknown, fallback: string): string => + typeof value === 'string' || typeof value === 'number' ? String(value) : fallback; + +/** The form from the admin's app settings; a key the server leaves out has its default. */ +export function formOf(settings: Record): MailAdminForm { + const transport = str(settings[KEY.transport], 'off'); + const tls = str(settings[KEY.imap_tls], 'tls'); + return { + transport: transport === 'imap' || transport === 'webhook' ? transport : 'off', + address_template: str(settings[KEY.address_template], ''), + imap_host: str(settings[KEY.imap_host], ''), + imap_port: str(settings[KEY.imap_port], '993'), + imap_tls: tls === 'starttls' ? 'starttls' : 'tls', + imap_user: str(settings[KEY.imap_user], ''), + imap_pass: str(settings[KEY.imap_pass], ''), + imap_mailbox: str(settings[KEY.imap_mailbox], 'INBOX'), + webhook_secret: str(settings[KEY.webhook_secret], ''), + ai_per_user_daily: str(settings[KEY.ai_per_user_daily], '10'), + }; +} + +export function enabledOf(settings: Record): boolean { + return String(settings[ENABLED_KEY] ?? 'false') === 'true'; +} + +/** Only what changed, under the stored keys, so a save never touches a field the admin left alone. */ +export function changesOf(form: MailAdminForm, saved: MailAdminForm): Record { + const patch: Record = {}; + for (const field of Object.keys(KEY) as MailAdminField[]) { + if (form[field] !== saved[field]) patch[KEY[field]] = form[field].trim(); + } + return patch; +} + +/** The fields the chosen transport shows, so the form never asks for what will not be used. */ +export function fieldsOf(transport: MailAdminTransport): MailAdminField[] { + switch (transport) { + case 'imap': + return ['imap_host', 'imap_port', 'imap_tls', 'imap_user', 'imap_pass', 'imap_mailbox']; + case 'webhook': + return ['webhook_secret']; + case 'off': + return []; + } +} + +export type MailAdminProblem = 'secretShort' | 'portInvalid' | 'templateToken' | 'dailyInvalid'; + +/** What stops a save, checked here so the admin hears it before the server answers. */ +export function problemsOf(form: MailAdminForm): MailAdminProblem[] { + const problems: MailAdminProblem[] = []; + const secret = form.webhook_secret.trim(); + if (form.transport === 'webhook' && secret !== MASKED && secret.length > 0 && secret.length < WEBHOOK_SECRET_MIN) { + problems.push('secretShort'); + } + const port = Number(form.imap_port); + if (form.transport === 'imap' && !(Number.isInteger(port) && port > 0 && port < 65536)) problems.push('portInvalid'); + if (form.address_template.trim() && !form.address_template.includes('{token}')) problems.push('templateToken'); + const daily = Number(form.ai_per_user_daily); + if (!(Number.isInteger(daily) && daily >= 0 && daily <= 1000)) problems.push('dailyInvalid'); + return problems; +} + +/** The status card in numbers: mails of the last day, added on their own, open entries. */ +export function statusFacts(status: MailIngestAdminStatusResponse): { + mails: number; + added: number; + waiting: number; + failed: number; +} { + const byStatus = status.last_day.by_status; + const mails = Object.values(byStatus).reduce((sum, n) => sum + (n ?? 0), 0); + return { + mails, + added: status.auto_applied, + waiting: status.backlog.reduce((sum, n) => sum + n, 0), + failed: byStatus.failed ?? 0, + }; +} + +/** A problem in the admin's words; a switch of literal keys so the i18n check sees each one. */ +export function problemText(problem: MailAdminProblem, t: TranslationFn): string { + switch (problem) { + case 'secretShort': + return t('mailAdmin.problem.secretShort'); + case 'portInvalid': + return t('mailAdmin.problem.portInvalid'); + case 'templateToken': + return t('mailAdmin.problem.templateToken', { token: '{token}' }); + case 'dailyInvalid': + return t('mailAdmin.problem.dailyInvalid'); + } +} + +export const TRANSPORTS: MailAdminTransport[] = ['off', 'imap', 'webhook']; + +export function transportLabel(transport: MailAdminTransport, t: TranslationFn): string { + switch (transport) { + case 'off': + return t('mailAdmin.transport.off'); + case 'imap': + return t('mailAdmin.transport.imap'); + case 'webhook': + return t('mailAdmin.transport.webhook'); + } +} diff --git a/client/src/components/Admin/mailIngest/useMailIngestAdmin.ts b/client/src/components/Admin/mailIngest/useMailIngestAdmin.ts new file mode 100644 index 000000000..5f1390cd9 --- /dev/null +++ b/client/src/components/Admin/mailIngest/useMailIngestAdmin.ts @@ -0,0 +1,124 @@ +import type { MailIngestAdminStatusResponse, MailIngestAdminTestResponse } from '@trek/shared'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from '../../../i18n'; +import { mailIngestRepo } from '../../../repo/mailIngestRepo'; +import { getApiErrorMessage } from '../../../types'; +import { useToast } from '../../shared/Toast'; +import { + ENABLED_KEY, + changesOf, + enabledOf, + formOf, + problemsOf, + type MailAdminField, + type MailAdminForm, +} from './mailIngestAdminModel'; + +/** + * The admin card of the booking mail import, desktop and phone alike: the + * instance switch (saved at once, as every switch in the admin settings), the + * transport fields (saved together, only the changed ones), the connection + * test and the status in counts. Loads once when shown. + */ +export function useMailIngestAdmin(active: boolean) { + const { t } = useTranslation(); + const toast = useToast(); + const [enabled, setEnabled] = useState(false); + const [saved, setSaved] = useState(null); + const [form, setForm] = useState(null); + const [status, setStatus] = useState(null); + const [test, setTest] = useState(null); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + + const loadStatus = useCallback(async () => { + try { + setStatus(await mailIngestRepo.adminStatus()); + } catch { + setStatus(null); + } + }, []); + + useEffect(() => { + if (!active) return; + let cancelled = false; + void (async () => { + try { + const raw = await mailIngestRepo.adminSettings(); + if (cancelled) return; + const loaded = formOf(raw); + setEnabled(enabledOf(raw)); + setSaved(loaded); + setForm(loaded); + } catch { + if (!cancelled) toast.error(t('mailAdmin.loadError')); + } + if (!cancelled) await loadStatus(); + })(); + return () => { + cancelled = true; + }; + }, [active, loadStatus, t, toast]); + + const toggle = async (on: boolean) => { + setEnabled(on); + try { + await mailIngestRepo.saveAdminSettings({ [ENABLED_KEY]: on }); + void loadStatus(); + } catch (err: unknown) { + setEnabled(!on); + toast.error(getApiErrorMessage(err, t('common.error'))); + } + }; + + const set = (field: K, value: MailAdminForm[K]) => + setForm((current) => (current ? { ...current, [field]: value } : current)); + + const patch = form && saved ? changesOf(form, saved) : {}; + const problems = form ? problemsOf(form) : []; + + const save = async () => { + if (!form || Object.keys(patch).length === 0 || problems.length > 0) return; + setSaving(true); + try { + await mailIngestRepo.saveAdminSettings(patch); + setSaved(form); + setTest(null); + toast.success(t('common.saved')); + void loadStatus(); + } catch (err: unknown) { + toast.error(getApiErrorMessage(err, t('common.error'))); + } finally { + setSaving(false); + } + }; + + const runTest = async () => { + setTesting(true); + try { + setTest(await mailIngestRepo.adminTest()); + } catch (err: unknown) { + setTest({ transport: form?.transport ?? 'off', ok: false, error: getApiErrorMessage(err, t('common.error')) }); + } finally { + setTesting(false); + } + }; + + return { + loaded: form !== null, + enabled, + toggle: (on: boolean) => void toggle(on), + form, + set, + dirty: Object.keys(patch).length > 0, + problems, + saving, + save: () => void save(), + status, + test, + testing, + runTest: () => void runTest(), + }; +} + +export type MailIngestAdmin = ReturnType; diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index 224d8bd1c..e8fa13459 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -14,8 +14,9 @@ import AssistantLauncher from '../Assistant/AssistantLauncher' import { resolvePluginIcon } from '../shared/PluginIcon' import { visibleManagedNavItems } from '../../managed' import { useCollapseOnOverflow } from '../../hooks/useCollapseOnOverflow' -import { useMailIngestStore } from '../../store/mailIngestStore' +import { useMailIngestStore, watchMailInbox } from '../../store/mailIngestStore' import { openMailInbox } from '../MailIngest/mailInboxOpener' +import { useMailInboxLink } from '../MailIngest/useMailInboxLink' import MailInboxDialog from '../MailIngest/MailInboxDialog' const ADDON_ICONS: Record = { CalendarDays, Briefcase, Globe, Compass, Bookmark } @@ -50,9 +51,12 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onSettings const mailImport = useMailIngestStore(s => s.settings) const mailPending = useMailIngestStore(s => s.pending) const mailImportOn = !!mailImport?.available && mailImport.prefs.enabled + useMailInboxLink(mailImportOn) const userId = user?.id useEffect(() => { - if (userId) void useMailIngestStore.getState().loadSettings() + if (!userId) return + void useMailIngestStore.getState().loadSettings() + return watchMailInbox() }, [userId]) const darkMode = settings.dark_mode const dark = darkMode === true || darkMode === 'dark' || (darkMode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) @@ -340,10 +344,10 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onSettings {/* Bookings from forwarded mails that wait for a decision; only where the import is on. */} {mailImportOn && ( )} diff --git a/client/src/components/MailIngest/MailInboxDialog.tsx b/client/src/components/MailIngest/MailInboxDialog.tsx index a4b2a4423..91ef3f20d 100644 --- a/client/src/components/MailIngest/MailInboxDialog.tsx +++ b/client/src/components/MailIngest/MailInboxDialog.tsx @@ -1,5 +1,5 @@ import type { MailIngestInboxEntry } from '@trek/shared'; -import { Copy, FilePlus2, FileText, Inbox, Loader2, Mail, PencilLine, Plus, Sparkles, Trash2 } from 'lucide-react'; +import { Copy, FilePlus2, FileText, Inbox, Loader2, Mail, PencilLine, Plus, Trash2 } from 'lucide-react'; import { useId, type ReactElement } from 'react'; import { useTranslation } from '../../i18n'; import { copyText } from '../../utils/clipboard'; @@ -28,8 +28,6 @@ function actionLook(action: InboxAction, t: (key: string) => string): { icon: Re return { icon: , label: t('mailInbox.action.file') }; case 'manual': return { icon: , label: t('mailInbox.action.manual') }; - case 'readAi': - return { icon: , label: t('mailInbox.action.readAi') }; case 'discard': return { icon: , label: t('mailInbox.action.discard') }; } @@ -75,8 +73,8 @@ function Entry({ entry, inbox }: { entry: MailIngestInboxEntry; inbox: ReturnTyp } - onClick={async () => { - if (await copyText(code)) toast.success(t('common.copied')); + onClick={() => { + void copyText(code).then((ok) => ok && toast.success(t('common.copied'))); }} /> diff --git a/client/src/components/MailIngest/TripMailForward.tsx b/client/src/components/MailIngest/TripMailForward.tsx index c04c59502..d320e21ff 100644 --- a/client/src/components/MailIngest/TripMailForward.tsx +++ b/client/src/components/MailIngest/TripMailForward.tsx @@ -1,8 +1,8 @@ -import { mailIngestAddressResponseSchema, type MailIngestAddress } from '@trek/shared'; +import type { MailIngestAddress } from '@trek/shared'; import { Copy, Mail, Plus } from 'lucide-react'; import { useEffect, useState, type ReactElement } from 'react'; -import { mailIngestApi } from '../../api/mailIngest'; import { useTranslation } from '../../i18n'; +import { mailIngestRepo } from '../../repo/mailIngestRepo'; import { useMailIngestStore } from '../../store/mailIngestStore'; import { copyText } from '../../utils/clipboard'; import { useToast } from '../shared/Toast'; @@ -30,13 +30,15 @@ export default function TripMailForward({ tripId }: { tripId: number }): ReactEl useEffect(() => { if (!on) return; let live = true; - mailIngestApi + mailIngestRepo .tripAddress(tripId) - .then((raw) => { - const parsed = mailIngestAddressResponseSchema.safeParse(raw); - if (live && parsed.success) setAddress(parsed.data.address); + .then((found) => { + if (live) setAddress(found); }) - .catch(() => {}); + .catch(() => { + // No address yet, or none readable: the card offers to create one instead. + if (live) setAddress(null); + }); return () => { live = false; }; @@ -47,7 +49,7 @@ export default function TripMailForward({ tripId }: { tripId: number }): ReactEl const create = async () => { setCreating(true); try { - setAddress(mailIngestAddressResponseSchema.parse(await mailIngestApi.createTripAddress(tripId)).address); + setAddress(await mailIngestRepo.createTripAddress(tripId)); } catch { toast.error(t('mailInbox.settings.saveError')); } finally { diff --git a/client/src/components/MailIngest/mailInboxModel.test.ts b/client/src/components/MailIngest/mailInboxModel.test.ts index ad4c98789..24e8f0df6 100644 --- a/client/src/components/MailIngest/mailInboxModel.test.ts +++ b/client/src/components/MailIngest/mailInboxModel.test.ts @@ -52,20 +52,19 @@ function entry( describe('mailInboxModel', () => { it('FE-MAILINBOX-MODEL-001: a read booking with a suggested trip is added in one tap, with every way out after it', () => { - expect(actionsOf(entry(), false)).toEqual(['add', 'file', 'manual', 'discard']); + expect(actionsOf(entry())).toEqual(['add', 'file', 'manual', 'discard']); }); it('FE-MAILINBOX-MODEL-002: without a suggestion the person chooses the trip', () => { - expect(actionsOf(entry({ suggested_trip: null }), false)[0]).toBe('choose'); + expect(actionsOf(entry({ suggested_trip: null }))[0]).toBe('choose'); }); - it('FE-MAILINBOX-MODEL-003: a mail without a booking offers the document, the form, and AI only where it can help', () => { + it('FE-MAILINBOX-MODEL-003: a mail without a booking offers the document and the form', () => { const none = entry( { item: null, reason: 'not_a_booking' }, { attachments: [{ file_name: 'invoice.pdf', kind: 'pdf', size: 1 }] } ); - expect(actionsOf(none, false)).toEqual(['file', 'manual', 'discard']); - expect(actionsOf(none, true)).toEqual(['file', 'manual', 'readAi', 'discard']); + expect(actionsOf(none)).toEqual(['file', 'manual', 'discard']); }); it('FE-MAILINBOX-MODEL-004: a forwarding confirmation or a newsletter only informs', () => { diff --git a/client/src/components/MailIngest/mailInboxModel.ts b/client/src/components/MailIngest/mailInboxModel.ts index d592d682a..e8b44619a 100644 --- a/client/src/components/MailIngest/mailInboxModel.ts +++ b/client/src/components/MailIngest/mailInboxModel.ts @@ -8,107 +8,71 @@ import type { MailIngestInboxEntry, MailIngestReason } from '@trek/shared'; */ /** The ways out of an entry, in the order the row offers them. */ -export type InboxAction = 'add' | 'choose' | 'file' | 'manual' | 'readAi' | 'discard'; - -/** Reasons where reading the mail again with AI is worth a try. */ -const AI_RETRY: ReadonlySet = new Set([ - 'ai_quota_exhausted', - 'ai_user_cap', - 'ai_provider_error', - 'ai_disabled', - 'not_a_booking', - 'mail_unreadable', -]); +export type InboxAction = 'add' | 'choose' | 'file' | 'manual' | 'discard'; /** Reasons that need no booking decision at all: the entry only informs. */ const INFO_ONLY: ReadonlySet = new Set(['forwarding_confirmation', 'newsletter', 'auto_reply']); -export function actionsOf(entry: MailIngestInboxEntry, aiAvailable: boolean): InboxAction[] { +/** + * A passing AI failure is read again by the server on its own; an allowance + * used up leaves the form and the document, so there is no "read again" here. + */ +export function actionsOf(entry: MailIngestInboxEntry): InboxAction[] { const reason = entry.reason ?? entry.message.reason; if (reason && INFO_ONLY.has(reason)) return ['discard']; const actions: InboxAction[] = []; if (entry.item) actions.push(entry.suggested_trip ? 'add' : 'choose'); if (entry.message.attachments.length > 0 || entry.item) actions.push('file'); actions.push('manual'); - if (aiAvailable && reason && AI_RETRY.has(reason)) actions.push('readAi'); actions.push('discard'); return actions; } +type Translate = (key: string) => string; + /** - * A reason in the reader's words. A switch of literal keys rather than a table, - * so the i18n check sees every key in use and a new code fails the type check - * here first. + * A reason in the reader's words. A table of literal keys, so the i18n check + * sees every key in use, and typed by the reason codes, so a new code fails + * the type check here first. */ -export function reasonText(reason: MailIngestReason, t: (key: string) => string): string { - switch (reason) { - case 'forwarding_confirmation': - return t('mailInbox.reason.forwardingConfirmation'); - case 'newsletter': - return t('mailInbox.reason.newsletter'); - case 'auto_reply': - return t('mailInbox.reason.autoReply'); - case 'empty_mail': - return t('mailInbox.reason.emptyMail'); - case 'mail_unreadable': - return t('mailInbox.reason.mailUnreadable'); - case 'not_a_booking': - return t('mailInbox.reason.notABooking'); - case 'too_large': - return t('mailInbox.reason.tooLarge'); - case 'address_invalid': - return t('mailInbox.reason.addressInvalid'); - case 'address_daily_limit': - return t('mailInbox.reason.addressDailyLimit'); - case 'processing_failed': - return t('mailInbox.reason.processingFailed'); - case 'ai_quota_exhausted': - return t('mailInbox.reason.aiQuotaExhausted'); - case 'ai_disabled': - return t('mailInbox.reason.aiDisabled'); - case 'ai_user_cap': - return t('mailInbox.reason.aiUserCap'); - case 'ai_provider_error': - return t('mailInbox.reason.aiProviderError'); - case 'no_trip_in_range': - return t('mailInbox.reason.noTripInRange'); - case 'trip_ambiguous': - return t('mailInbox.reason.tripAmbiguous'); - case 'trip_without_dates': - return t('mailInbox.reason.tripWithoutDates'); - case 'no_edit_permission': - return t('mailInbox.reason.noEditPermission'); - case 'demo_mode': - return t('mailInbox.reason.demoMode'); - case 'outside_trip_dates': - return t('mailInbox.reason.outsideTripDates'); - case 'source_mismatch': - return t('mailInbox.reason.sourceMismatch'); - case 'implausible': - return t('mailInbox.reason.implausible'); - case 'not_in_text': - return t('mailInbox.reason.notInText'); - case 'image_only': - return t('mailInbox.reason.imageOnly'); - case 'missing_critical_field': - return t('mailInbox.reason.missingCriticalField'); - case 'missing_confirmation': - return t('mailInbox.reason.missingConfirmation'); - case 'too_many_bookings': - return t('mailInbox.reason.tooManyBookings'); - case 'change_or_cancellation': - return t('mailInbox.reason.changeOrCancellation'); - case 'cancelled': - return t('mailInbox.reason.cancelled'); - case 'possible_duplicate': - return t('mailInbox.reason.possibleDuplicate'); - case 'change_available': - return t('mailInbox.reason.changeAvailable'); - case 'conflict': - return t('mailInbox.reason.conflict'); - case 'ask_first': - return t('mailInbox.reason.askFirst'); - } +const REASON_TEXT: Record string> = { + forwarding_confirmation: (t) => t('mailInbox.reason.forwardingConfirmation'), + newsletter: (t) => t('mailInbox.reason.newsletter'), + auto_reply: (t) => t('mailInbox.reason.autoReply'), + empty_mail: (t) => t('mailInbox.reason.emptyMail'), + mail_unreadable: (t) => t('mailInbox.reason.mailUnreadable'), + not_a_booking: (t) => t('mailInbox.reason.notABooking'), + too_large: (t) => t('mailInbox.reason.tooLarge'), + address_invalid: (t) => t('mailInbox.reason.addressInvalid'), + address_daily_limit: (t) => t('mailInbox.reason.addressDailyLimit'), + processing_failed: (t) => t('mailInbox.reason.processingFailed'), + ai_quota_exhausted: (t) => t('mailInbox.reason.aiQuotaExhausted'), + ai_disabled: (t) => t('mailInbox.reason.aiDisabled'), + ai_user_cap: (t) => t('mailInbox.reason.aiUserCap'), + ai_provider_error: (t) => t('mailInbox.reason.aiProviderError'), + no_trip_in_range: (t) => t('mailInbox.reason.noTripInRange'), + trip_ambiguous: (t) => t('mailInbox.reason.tripAmbiguous'), + trip_without_dates: (t) => t('mailInbox.reason.tripWithoutDates'), + no_edit_permission: (t) => t('mailInbox.reason.noEditPermission'), + demo_mode: (t) => t('mailInbox.reason.demoMode'), + outside_trip_dates: (t) => t('mailInbox.reason.outsideTripDates'), + source_mismatch: (t) => t('mailInbox.reason.sourceMismatch'), + implausible: (t) => t('mailInbox.reason.implausible'), + not_in_text: (t) => t('mailInbox.reason.notInText'), + image_only: (t) => t('mailInbox.reason.imageOnly'), + missing_critical_field: (t) => t('mailInbox.reason.missingCriticalField'), + missing_confirmation: (t) => t('mailInbox.reason.missingConfirmation'), + too_many_bookings: (t) => t('mailInbox.reason.tooManyBookings'), + change_or_cancellation: (t) => t('mailInbox.reason.changeOrCancellation'), + cancelled: (t) => t('mailInbox.reason.cancelled'), + possible_duplicate: (t) => t('mailInbox.reason.possibleDuplicate'), + change_available: (t) => t('mailInbox.reason.changeAvailable'), + conflict: (t) => t('mailInbox.reason.conflict'), + ask_first: (t) => t('mailInbox.reason.askFirst'), +}; + +export function reasonText(reason: MailIngestReason, t: Translate): string { + return REASON_TEXT[reason](t); } /** The entry's headline: the booking's title, else the mail's subject, else nothing to show. */ diff --git a/client/src/components/MailIngest/useMailImportSettings.ts b/client/src/components/MailIngest/useMailImportSettings.ts index 72dd8dd3f..e9b884418 100644 --- a/client/src/components/MailIngest/useMailImportSettings.ts +++ b/client/src/components/MailIngest/useMailImportSettings.ts @@ -1,7 +1,7 @@ -import { mailIngestSettingsResponseSchema, mailIngestUploadResponseSchema, type MailIngestPrefs } from '@trek/shared'; +import type { MailIngestPrefs } from '@trek/shared'; import { useEffect, useRef, useState } from 'react'; -import { mailIngestApi } from '../../api/mailIngest'; import { useTranslation } from '../../i18n'; +import { mailIngestRepo } from '../../repo/mailIngestRepo'; import { useMailIngestStore } from '../../store/mailIngestStore'; import { copyText } from '../../utils/clipboard'; import { useToast } from '../shared/Toast'; @@ -37,10 +37,7 @@ export function useMailImportSettings() { if (!settings || saving) return; setSaving(key); try { - const parsed = mailIngestSettingsResponseSchema.parse( - await mailIngestApi.updateSettings({ [key]: !settings.prefs[key] }) - ); - setSettings(parsed); + setSettings(await mailIngestRepo.updateSettings({ [key]: !settings.prefs[key] })); } catch { toast.error(t('mailInbox.settings.saveError')); } finally { @@ -64,7 +61,7 @@ export function useMailImportSettings() { setRenewArmed(false); setRenewing(true); try { - await mailIngestApi.renewAddress(); + await mailIngestRepo.renewAddress(); await loadSettings(); toast.success(t('mailInbox.settings.renewed')); } catch { @@ -77,7 +74,7 @@ export function useMailImportSettings() { const upload = async (file: File) => { setUploading(true); try { - const { duplicate } = mailIngestUploadResponseSchema.parse(await mailIngestApi.upload(file)); + const { duplicate } = await mailIngestRepo.upload(file); toast.success(duplicate ? t('mailInbox.settings.uploadDuplicate') : t('mailInbox.settings.uploaded')); } catch { toast.error(t('mailInbox.settings.uploadError')); diff --git a/client/src/components/MailIngest/useMailInbox.ts b/client/src/components/MailIngest/useMailInbox.ts index ece72830b..73cc14497 100644 --- a/client/src/components/MailIngest/useMailInbox.ts +++ b/client/src/components/MailIngest/useMailInbox.ts @@ -1,13 +1,11 @@ -import { - mailIngestMessagePreviewResponseSchema, - type MailIngestInboxEntry, - type MailIngestMessagePreviewResponse, -} from '@trek/shared'; +import type { MailIngestInboxEntry, MailIngestMessagePreviewResponse } from '@trek/shared'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; -import { tripsApi } from '../../api/client'; -import { mailIngestApi, type MailIngestAction } from '../../api/mailIngest'; +import { useShallow } from 'zustand/react/shallow'; +import type { MailIngestAction } from '../../api/mailIngest'; import { useTranslation } from '../../i18n'; +import { mailIngestRepo } from '../../repo/mailIngestRepo'; +import { tripRepo } from '../../repo/tripRepo'; import { useMailIngestStore } from '../../store/mailIngestStore'; import { useToast } from '../shared/Toast'; import { actionsOf, type InboxAction } from './mailInboxModel'; @@ -32,7 +30,18 @@ export function useMailInbox() { const toast = useToast(); const navigate = useNavigate(); const open = useMailInboxOpen((s) => s.open); - const { entries, pending, nextBefore, loadingInbox, settings, loadInbox, loadSettings, act } = useMailIngestStore(); + const { entries, pending, nextBefore, loadingInbox, settings, loadInbox, loadSettings, act } = useMailIngestStore( + useShallow((s) => ({ + entries: s.entries, + pending: s.pending, + nextBefore: s.nextBefore, + loadingInbox: s.loadingInbox, + settings: s.settings, + loadInbox: s.loadInbox, + loadSettings: s.loadSettings, + act: s.act, + })) + ); const [trips, setTrips] = useState([]); const [busy, setBusy] = useState(null); /** The entry and the action waiting for a trip to be picked. */ @@ -45,10 +54,17 @@ export function useMailInbox() { if (!open) return; void loadInbox(); if (!settings) void loadSettings(); - tripsApi + tripRepo .list() - .then((d: { trips?: InboxTrip[] }) => - setTrips((d.trips ?? []).map(({ id, title, start_date, end_date }) => ({ id, title, start_date, end_date }))) + .then(({ trips: active }) => + setTrips( + active.map(({ id, title, start_date, end_date }) => ({ + id, + title, + start_date: start_date ?? null, + end_date: end_date ?? null, + })) + ) ) .catch(() => setTrips([])); }, [open, loadInbox, loadSettings, settings]); @@ -81,8 +97,6 @@ export function useMailInbox() { return tripId ? run(entry, 'file', { trip_id: tripId }, t('mailInbox.filed')) : setPicking({ entry, action: 'file' }); - case 'readAi': - return run(entry, 'read-ai', {}, t('mailInbox.readAgain')); case 'discard': return run(entry, 'discard', {}, t('mailInbox.discarded')); case 'manual': @@ -100,7 +114,7 @@ export function useMailInbox() { const openForm = (entry: MailIngestInboxEntry, tripId: number) => { closeMailInbox(); - navigate(`/trips/${tripId}?mailPrefill=${entry.id}`); + void navigate(`/trips/${tripId}?mailPrefill=${entry.id}`); }; const fillIn = (entry: MailIngestInboxEntry) => { @@ -111,13 +125,12 @@ export function useMailInbox() { const showMail = async (entry: MailIngestInboxEntry) => { try { - setPreview(mailIngestMessagePreviewResponseSchema.parse(await mailIngestApi.preview(entry.message_id))); + setPreview(await mailIngestRepo.preview(entry.message_id)); } catch { toast.error(t('mailInbox.previewError')); } }; - const aiAvailable = !!settings?.ai_available && !!settings.prefs.ai_enabled; return { open, close: closeMailInbox, @@ -128,7 +141,7 @@ export function useMailInbox() { loading: loadingInbox, trips, busy, - actionsOf: (entry: MailIngestInboxEntry) => actionsOf(entry, aiAvailable), + actionsOf, perform: (entry: MailIngestInboxEntry, action: InboxAction) => void perform(entry, action), picking, pick, diff --git a/client/src/components/MailIngest/useMailInboxLink.ts b/client/src/components/MailIngest/useMailInboxLink.ts new file mode 100644 index 000000000..9562d44c6 --- /dev/null +++ b/client/src/components/MailIngest/useMailInboxLink.ts @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router'; +import { openMailInbox } from './mailInboxOpener'; + +/** The query a link uses to open the inbox, e.g. the "bookings waiting" notification's `/dashboard?inbox=mail`. */ +const INBOX_PARAM = 'inbox'; +const INBOX_MAIL = 'mail'; + +/** + * Opens the inbox of forwarded bookings when the address asks for it, then + * takes the query off again so a reload or the back button does not reopen + * it. Mounted where the inbox itself is mounted: the Navbar on the desktop, + * MMailInboxHost on the phone. + */ +export function useMailInboxLink(enabled: boolean): void { + const location = useLocation(); + const navigate = useNavigate(); + useEffect(() => { + if (!enabled) return; + const params = new URLSearchParams(location.search); + if (params.get(INBOX_PARAM) !== INBOX_MAIL) return; + openMailInbox(); + params.delete(INBOX_PARAM); + const search = params.toString(); + void navigate( + { pathname: location.pathname, search: search ? `?${search}` : '', hash: location.hash }, + { replace: true } + ); + }, [enabled, location.pathname, location.search, location.hash, navigate]); +} diff --git a/client/src/components/MailIngest/useMailPrefillLink.ts b/client/src/components/MailIngest/useMailPrefillLink.ts index c5c7697d8..ac89c0586 100644 --- a/client/src/components/MailIngest/useMailPrefillLink.ts +++ b/client/src/components/MailIngest/useMailPrefillLink.ts @@ -1,11 +1,9 @@ -import { - isTransportReservationType, - mailIngestPrefillResponseSchema, - type BookingImportPreviewItem, -} from '@trek/shared'; +import { isTransportReservationType, type BookingImportPreviewItem } from '@trek/shared'; import { useEffect, useRef } from 'react'; import type { SetURLSearchParams } from 'react-router'; -import { mailIngestApi } from '../../api/mailIngest'; +import { useTranslation } from '../../i18n'; +import { mailIngestRepo } from '../../repo/mailIngestRepo'; +import { useToast } from '../shared/Toast'; /** * "Fill in yourself" from the mail inbox lands in the trip as ?mailPrefill=: @@ -25,6 +23,8 @@ export function useMailPrefillLink({ loaded: boolean; startImportReview: (items: BookingImportPreviewItem[], files?: File[], kind?: 'transports' | 'bookings') => void; }): void { + const { t } = useTranslation(); + const toast = useToast(); const requested = searchParams.get('mailPrefill'); const handled = useRef(null); @@ -41,14 +41,14 @@ export function useMailPrefillLink({ ); const id = Number(requested); if (!Number.isInteger(id) || id <= 0) return; - mailIngestApi + mailIngestRepo .prefill(id) - .then((raw) => { - const { item } = mailIngestPrefillResponseSchema.parse(raw); + .then(({ item }) => { startImportReview([item], [], isTransportReservationType(item.type) ? 'transports' : 'bookings'); }) .catch(() => { - // An entry decided meanwhile has nothing to prefill; the trip simply stays open. + // An entry decided meanwhile has nothing to prefill: the trip stays open, and the person hears why. + toast.error(t('mailInbox.actionError')); }); - }, [requested, loaded, setSearchParams, startImportReview]); + }, [requested, loaded, setSearchParams, startImportReview, t, toast]); } diff --git a/client/src/components/Notifications/notificationModel.ts b/client/src/components/Notifications/notificationModel.ts index 20ad0e8bd..6b6b0efc2 100644 --- a/client/src/components/Notifications/notificationModel.ts +++ b/client/src/components/Notifications/notificationModel.ts @@ -41,6 +41,9 @@ const KIND_BY_TITLE: Record = { 'notif.flight_departed.title': 'flight', 'notif.flight_delayed.title': 'flight', 'notif.flight_landed.title': 'flight', + 'notif.mail_booking_added.title': 'booking', + 'notif.mail_booking_pending.title': 'booking', + 'notif.mail_ingest_paused.title': 'alert', 'notif.packing_tagged.title': 'packing', 'notif.todo_due.title': 'todo', 'notif.vacay_invite.title': 'vacay', diff --git a/client/src/components/Planner/bookings/BookingDetailDialog.tsx b/client/src/components/Planner/bookings/BookingDetailDialog.tsx index 141059002..7c0aaea92 100644 --- a/client/src/components/Planner/bookings/BookingDetailDialog.tsx +++ b/client/src/components/Planner/bookings/BookingDetailDialog.tsx @@ -110,6 +110,15 @@ export default function BookingDetailDialog(p: BookingDetailDialogProps) { if (await copyText(r.confirmation_number || '')) toast.success(t('common.copied')) else toast.error(t('reservations.copyFailed')) } + // "Looks right": the person checked what an import could not read for sure. + const confirmReview = async () => { + try { + await updateReservation(p.tripId, r.id, { needs_review: 0 }) + toast.success(t('reservations.reviewConfirmed')) + } catch { + toast.error(t('reservations.toast.updateError')) + } + } const rename = async () => { const title = name.trim() setRenaming(false) @@ -143,7 +152,7 @@ export default function BookingDetailDialog(p: BookingDetailDialogProps) { {facts.day.date && !facts.isHotel && {facts.day.date}} )} - {!!r.needs_review && } + {!!r.needs_review && void confirmReview() : undefined} />} {r.confirmation_number && ( diff --git a/client/src/components/Planner/bookings/bookingParts.test.tsx b/client/src/components/Planner/bookings/bookingParts.test.tsx index 8037b2bcf..91ad9cef1 100644 --- a/client/src/components/Planner/bookings/bookingParts.test.tsx +++ b/client/src/components/Planner/bookings/bookingParts.test.tsx @@ -1,11 +1,11 @@ -// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-004 +// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-005 import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { buildTripFile } from '../../../../tests/helpers/factories'; import { downloadFile, openFile } from '../../../utils/fileDownload'; import type { BookingFacts } from './bookingFacts'; -import { FileRows, WhenFields } from './bookingParts'; +import { FileRows, ReviewPill, WhenFields } from './bookingParts'; vi.mock('../../../utils/fileDownload', () => ({ openFile: vi.fn(() => Promise.resolve()), @@ -88,4 +88,13 @@ describe('FileRows', () => { const { container } = render(); expect(container).toBeEmptyDOMElement(); }); + + it('FE-PLANNER-BKPARTS-005: the review pill takes its mark off only for someone who may edit', () => { + const onConfirm = vi.fn(); + const { rerender } = render(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + rerender(); + fireEvent.click(screen.getByRole('button')); + expect(onConfirm).toHaveBeenCalledOnce(); + }); }); diff --git a/client/src/components/Planner/bookings/bookingParts.tsx b/client/src/components/Planner/bookings/bookingParts.tsx index 3c5f8e776..017d5ee6a 100644 --- a/client/src/components/Planner/bookings/bookingParts.tsx +++ b/client/src/components/Planner/bookings/bookingParts.tsx @@ -167,15 +167,33 @@ export function TypeTile({ type, size = 32, raised = false }: { type: string; si ) } -/** The warning icon beside a booking's name on a card or row, or with `pill` a header pill in the detail dialog. */ -export function ReviewPill({ pill = false }: { pill?: boolean }) { +/** + * The warning icon beside a booking's name on a card or row, or with `pill` a + * header pill in the detail dialog. With `onConfirm` (the right to edit) the + * pill is the "looks right" switch that takes the mark off. + */ +export function ReviewPill({ pill = false, onConfirm }: { pill?: boolean; onConfirm?: () => void }) { const { t } = useTranslation() + const content = ( + <> + + {t('reservations.needsReview')} + + ) + if (pill && onConfirm) { + return ( + + + + ) + } return ( {pill ? ( - - {t('reservations.needsReview')} + {content} ) : ( diff --git a/client/src/components/Planner/flightLive/FlightLiveStrip.tsx b/client/src/components/Planner/flightLive/FlightLiveStrip.tsx index 0f002e506..69a7123cc 100644 --- a/client/src/components/Planner/flightLive/FlightLiveStrip.tsx +++ b/client/src/components/Planner/flightLive/FlightLiveStrip.tsx @@ -1,5 +1,7 @@ +import { useTranslation } from '../../../i18n'; import type { Reservation } from '../../../types'; import type { DelayTone } from '../../../utils/flightLiveModel'; +import { BOX, Eyebrow } from '../bookings/bookingParts'; import { useCurrentFlightLegIndex, useFlightLiveCard } from './useFlightLiveCard'; const DELAY_TEXT: Record = { ok: 'text-success', minor: 'text-warning', major: 'text-danger' }; @@ -13,8 +15,9 @@ interface FlightLiveStripProps { } /** - * A flight's live state on the booking cards: what is happening in one line - * with the delay, and a thin bar across the card while it flies. The full + * A flight's live state on the booking cards, as one of the card's fields: + * what is happening in one line with the delay, and a thin bar along the + * field's foot while it flies. The full * picture is one click away in the booking dialog. Nothing far ahead of the * flight (the card shows its date already), nothing once it is long over. */ @@ -30,30 +33,30 @@ function Strip({ legIndex, className, }: Omit & { legIndex: number }) { + const { t } = useTranslation(); const card = useFlightLiveCard(tripId, reservationId, legIndex); if (!card || card.view.stage === 'upcoming') return null; const { view } = card; + // The card's own field: the eyebrow over a framed value, like the booking code and the route under it. return ( -
-
- {card.line} - {card.delay && card.delay.tone !== 'ok' && ( - - {card.delay.text} +
+ {t('reservations.status')} +
+
+ {card.line} + {card.delay && card.delay.tone !== 'ok' && ( + {card.delay.text} + )} +
+ {view.stage === 'airborne' && ( +
- {view.stage === 'airborne' && ( -
); } diff --git a/client/src/components/Planner/flightLive/flightMiniMap.css b/client/src/components/Planner/flightLive/flightMiniMap.css index 028d726d2..d860817b2 100644 --- a/client/src/components/Planner/flightLive/flightMiniMap.css +++ b/client/src/components/Planner/flightLive/flightMiniMap.css @@ -90,3 +90,14 @@ .flight-mini-map .leaflet-tooltip.flight-mini-code::before { display: none; } + +/* A map kept to the middle of its pass (the phone): the edges still run out, + but the route fills most of it instead of a third. */ +.flight-mini-map.flight-mini-map--narrow { + -webkit-mask-image: + linear-gradient(to right, transparent, #000 16%, #000 84%, transparent), + linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent); + mask-image: + linear-gradient(to right, transparent, #000 16%, #000 84%, transparent), + linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent); +} diff --git a/client/src/components/Settings/notificationLabels.ts b/client/src/components/Settings/notificationLabels.ts index 4ee220653..cfce21ef3 100644 --- a/client/src/components/Settings/notificationLabels.ts +++ b/client/src/components/Settings/notificationLabels.ts @@ -32,6 +32,9 @@ export const EVENT_LABEL_KEYS: Record = { flight_departed: 'settings.notifyFlightDeparted', flight_delayed: 'settings.notifyFlightDelayed', flight_landed: 'settings.notifyFlightLanded', + mail_booking_added: 'settings.notifyMailBookingAdded', + mail_booking_pending: 'settings.notifyMailBookingPending', + mail_ingest_paused: 'settings.notifyMailIngestPaused', } /** Plugin channels have no i18n; the server sends their display name outright. */ diff --git a/client/src/mobile/screens/admin/MAdminSettingsSection.tsx b/client/src/mobile/screens/admin/MAdminSettingsSection.tsx index 61997bd12..afb2210d7 100644 --- a/client/src/mobile/screens/admin/MAdminSettingsSection.tsx +++ b/client/src/mobile/screens/admin/MAdminSettingsSection.tsx @@ -7,6 +7,7 @@ import type { useAdmin } from '../../../pages/admin/useAdmin' import MToggle from '../../components/MToggle' import { MBlockDisclosure, MProviderBlock, MTrekApiBlock } from './MApiProviderBlocks' import MAdminOidcCard from './MAdminOidcCard' +import MMailIngestAdminCard from './MMailIngestAdminCard' import MSetPickerSheet from '../settings/MSetPickerSheet' import { MAdminButton, @@ -196,6 +197,9 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio onChange={(v) => void handleToggleAuthSetting('flight_live_enabled', v, setFlightLive)} /> + {/* Booking mail import: self-hosted only, the operator runs the TREK Cloud relay (desktop parity). */} + {!managed && } + {/* Allowed file types */} diff --git a/client/src/mobile/screens/admin/MMailIngestAdminCard.tsx b/client/src/mobile/screens/admin/MMailIngestAdminCard.tsx new file mode 100644 index 000000000..6a7ff7c25 --- /dev/null +++ b/client/src/mobile/screens/admin/MMailIngestAdminCard.tsx @@ -0,0 +1,215 @@ +import { + TRANSPORTS, + problemText, + statusFacts, + transportLabel, + type MailAdminForm, + type MailAdminProblem, +} from '../../../components/Admin/mailIngest/mailIngestAdminModel'; +import { useMailIngestAdmin, type MailIngestAdmin } from '../../../components/Admin/mailIngest/useMailIngestAdmin'; +import { useTranslation } from '../../../i18n'; +import type { TranslationFn } from '../../../types'; +import { relativeTime } from '../../../utils/relativeTime'; +import MSegmented from '../../components/MSegmented'; +import MToggle from '../../components/MToggle'; +import { + MAdminButton, + MAdminCard, + MAdminCardHead, + MAdminField, + MAdminInput, + MAdminRow, + MAdminSecretInput, +} from './MAdminUi'; + +interface PartProps { + admin: MailIngestAdmin; + form: MailAdminForm; + t: TranslationFn; +} + +/** The problem under a field, in the danger colour, or nothing. */ +function Problem({ admin, problem, t }: { admin: MailIngestAdmin; problem: MailAdminProblem; t: TranslationFn }) { + if (!admin.problems.includes(problem)) return null; + return

{problemText(problem, t)}

; +} + +function ImapFields({ admin, form, t }: PartProps) { + return ( + <> + + admin.set('imap_host', e.target.value)} + /> + +
+ + admin.set('imap_port', e.target.value)} + /> + + + + admin.set('imap_tls', value)} + options={[ + { value: 'tls', label: 'TLS' }, + { value: 'starttls', label: 'STARTTLS' }, + ]} + /> + +
+ + admin.set('imap_user', e.target.value)} + /> + + + admin.set('imap_pass', e.target.value)} + /> + + + admin.set('imap_mailbox', e.target.value)} /> + + + ); +} + +function WebhookFields({ admin, form, t }: PartProps) { + return ( + <> + + admin.set('webhook_secret', e.target.value)} + /> + + + +

{`${window.location.origin}/api/mail-ingest/inbound`}

+
+ + ); +} + +/** How the import runs, in counts only. */ +function Status({ admin, t, locale }: { admin: MailIngestAdmin; t: TranslationFn; locale: string }) { + const status = admin.status; + if (!status) return null; + const facts = statusFacts(status); + const lastRead = status.last_ok_at + ? relativeTime(Date.parse(status.last_ok_at), locale) + : t('mailAdmin.status.never'); + return ( +
+

{t('mailAdmin.status.lastRead', { time: lastRead })}

+
+ {[ + { label: t('mailAdmin.status.mails'), value: facts.mails }, + { label: t('mailAdmin.status.added'), value: facts.added }, + { label: t('mailAdmin.status.waiting'), value: facts.waiting }, + { label: t('mailAdmin.status.failed'), value: facts.failed }, + ].map((f) => ( +
+ {f.label} + {f.value} +
+ ))} +
+ {status.last_error &&

{status.last_error}

} +
+ ); +} + +function Setup({ admin, form, t, locale }: PartProps & { locale: string }) { + return ( +
+ + admin.set('transport', value)} + options={TRANSPORTS.map((x) => ({ value: x, label: transportLabel(x, t) }))} + /> + + + admin.set('address_template', e.target.value)} + /> + + + {form.transport === 'imap' && } + {form.transport === 'webhook' && } + + admin.set('ai_per_user_daily', e.target.value)} + /> + + +

{t('mailAdmin.envHint')}

+
+ 0} onClick={admin.save}> + {t('common.save')} + + + {t('mailAdmin.test.run')} + + {admin.test && ( + + {admin.test.ok ? t('mailAdmin.test.ok') : (admin.test.error ?? t('mailAdmin.test.failed'))} + + )} +
+ +
+ ); +} + +/** + * The booking mail import in the phone's admin settings, the same card as on + * the desktop (useMailIngestAdmin): the switch, how mail arrives, the address + * template, the AI allowance, the test and the status. Self-hosted only. + */ +export default function MMailIngestAdminCard() { + const { t, locale } = useTranslation(); + const admin = useMailIngestAdmin(true); + return ( + + + + } + /> + {admin.enabled && admin.form && } + + ); +} diff --git a/client/src/mobile/screens/mail/MMailInboxHost.tsx b/client/src/mobile/screens/mail/MMailInboxHost.tsx index 5b79a5117..58c3c919b 100644 --- a/client/src/mobile/screens/mail/MMailInboxHost.tsx +++ b/client/src/mobile/screens/mail/MMailInboxHost.tsx @@ -1,18 +1,24 @@ import { useEffect, type ReactElement } from 'react'; +import { useMailInboxLink } from '../../../components/MailIngest/useMailInboxLink'; import { useAuthStore } from '../../../store/authStore'; -import { useMailIngestStore } from '../../../store/mailIngestStore'; +import { useMailIngestStore, watchMailInbox } from '../../../store/mailIngestStore'; import MMailInboxSheet from './MMailInboxSheet'; /** * The phone shell's one inbox of bookings from forwarded mails: reads the * person's import settings once they are signed in (the user menu's badge - * needs them) and mounts the sheet only where the import is on. + * needs them), follows the server's inbox events, and mounts the sheet only + * where the import is on. */ export default function MMailInboxHost(): ReactElement | null { const userId = useAuthStore((s) => s.user?.id); const settings = useMailIngestStore((s) => s.settings); useEffect(() => { - if (userId) void useMailIngestStore.getState().loadSettings(); + if (!userId) return; + void useMailIngestStore.getState().loadSettings(); + return watchMailInbox(); }, [userId]); - return settings?.available && settings.prefs.enabled ? : null; + const on = !!settings?.available && settings.prefs.enabled; + useMailInboxLink(on); + return on ? : null; } diff --git a/client/src/mobile/screens/mail/MMailInboxSheet.tsx b/client/src/mobile/screens/mail/MMailInboxSheet.tsx index f59ad9d23..8cab90644 100644 --- a/client/src/mobile/screens/mail/MMailInboxSheet.tsx +++ b/client/src/mobile/screens/mail/MMailInboxSheet.tsx @@ -1,5 +1,5 @@ import type { MailIngestInboxEntry } from '@trek/shared'; -import { ChevronLeft, Copy, FilePlus2, FileText, Inbox, Mail, PencilLine, Plus, Sparkles, Trash2 } from 'lucide-react'; +import { ChevronLeft, Copy, FilePlus2, FileText, Inbox, Mail, PencilLine, Plus, Trash2 } from 'lucide-react'; import type { ReactElement } from 'react'; import { entryDates, entryTitle, reasonText, type InboxAction } from '../../../components/MailIngest/mailInboxModel'; import { useMailInbox } from '../../../components/MailIngest/useMailInbox'; @@ -23,8 +23,6 @@ function icon(action: InboxAction): ReactElement { return ; case 'manual': return ; - case 'readAi': - return ; case 'discard': return ; } @@ -40,8 +38,6 @@ function label(action: InboxAction, t: (key: string) => string): string { return t('mailInbox.action.file'); case 'manual': return t('mailInbox.action.manual'); - case 'readAi': - return t('mailInbox.action.readAi'); case 'discard': return t('mailInbox.action.discard'); } @@ -97,8 +93,8 @@ function Entry({ entry, inbox }: { entry: MailIngestInboxEntry; inbox: ReturnTyp type="button" aria-label={t('mailInbox.settings.copy')} className={ROUND} - onClick={async () => { - if (await copyText(code)) toast.success(t('common.copied')); + onClick={() => { + void copyText(code).then((ok) => ok && toast.success(t('common.copied'))); }} > diff --git a/client/src/mobile/screens/trip/MFlightLiveStrip.tsx b/client/src/mobile/screens/trip/MFlightLiveStrip.tsx index 5d3ca9bb2..528f0abe2 100644 --- a/client/src/mobile/screens/trip/MFlightLiveStrip.tsx +++ b/client/src/mobile/screens/trip/MFlightLiveStrip.tsx @@ -1,5 +1,7 @@ import { useCurrentFlightLegIndex, useFlightLiveCard } from '../../../components/Planner/flightLive/useFlightLiveCard'; +import { useTranslation } from '../../../i18n'; import type { DelayTone } from '../../../utils/flightLiveModel'; +import { Field } from './tabs/tabChrome'; const DELAY_COLOR: Record = { ok: 'var(--m-st-confirmed)', @@ -18,8 +20,9 @@ interface MFlightLiveStripProps { } /** - * A flight's live state on the phone's transport cards: what is happening in - * one line with the delay, and a thin bar across the card while it flies. A + * A flight's live state on the phone's transport cards, as one of the card's + * fields: what is happening in one line with the delay, and a thin bar along + * the field's foot while it flies. A * tap opens the transport sheet with the full card. Nothing far ahead of the * flight (the card shows its date already), nothing once it is long over. */ @@ -32,37 +35,31 @@ export default function MFlightLiveStrip({ tripId, reservationId, onOpen, classN } function Strip({ tripId, reservationId, legIndex, onOpen, className }: MFlightLiveStripProps & { legIndex: number }) { + const { t } = useTranslation(); const card = useFlightLiveCard(tripId, reservationId, legIndex); if (!card || card.view.stage === 'upcoming') return null; const { view } = card; + // One of the card's own fields, full width: the label over the framed value, like the booking code below it. return ( - ); } diff --git a/client/src/mobile/screens/trip/sheets/MFlightLiveCard.tsx b/client/src/mobile/screens/trip/sheets/MFlightLiveCard.tsx index 39f2efd03..e5554e7e9 100644 --- a/client/src/mobile/screens/trip/sheets/MFlightLiveCard.tsx +++ b/client/src/mobile/screens/trip/sheets/MFlightLiveCard.tsx @@ -127,15 +127,12 @@ export default function MFlightLiveCard({ tripId, reservationId, legIndex, legLa showPlane={view.stage !== 'upcoming' && view.stage !== 'soon'} dimPlane={view.stale} credits={card.attribution ? [card.attribution, FLIGHT_MAP_CREDIT] : [FLIGHT_MAP_CREDIT]} - inset={0.36} - className="!absolute inset-0 [--fm-ahead:var(--m-muted)] [--fm-code:var(--m-muted)] [--fm-end:var(--m-inner)] [--fm-flown:var(--m-st-info)] [--fm-halo:var(--m-inner)] [--fm-plane:var(--m-st-info)]" + inset={0.06} + className="flight-mini-map--narrow !absolute inset-y-2 end-[25%] start-[25%] [--fm-ahead:var(--m-muted)] [--fm-code:var(--m-muted)] [--fm-end:var(--m-inner)] [--fm-flown:var(--m-st-info)] [--fm-halo:var(--m-inner)] [--fm-plane:var(--m-st-info)]" /> -
- - {leg.flight_number} - -
+ {/* The middle column keeps the map clear; the flight number is in the sheet's title already. */} +
+ {/* ── An import could not read every detail for sure ── */} + {!!res.needs_review && ( + { + planner.tripActions.updateReservation(planner.tripId, res.id, { needs_review: 0 }) + .then(() => planner.toast.success(t('reservations.reviewConfirmed'))) + .catch(() => planner.toast.error(t('reservations.toast.updateError'))) + } : undefined} + /> + )} + {/* ── A stay: check-in, check-out, nights ── */} {hotelStats.length > 0 && (
diff --git a/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx b/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx index 4b4d78038..e8b0344fe 100644 --- a/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx @@ -158,7 +158,7 @@ function TransportCard({ res, planner, shell, canEdit, compact }: { {/* A flight TREK follows live: its moment in one line, on the compact card too. */} {res.type === 'flight' && ( - + )} {/* Body — split around the booking code so the reveal can be its own diff --git a/client/src/pages/admin/AdminSettingsTab.tsx b/client/src/pages/admin/AdminSettingsTab.tsx index 812ab3e82..ddb02f2c0 100644 --- a/client/src/pages/admin/AdminSettingsTab.tsx +++ b/client/src/pages/admin/AdminSettingsTab.tsx @@ -16,6 +16,7 @@ import GoogleOptions from './GoogleOptions' import GoogleDailyLimitRow from '../../components/Admin/GoogleDailyLimitRow' import ProviderBlock from './ProviderBlock' import TrekApiCard from './TrekApiCard' +import MailIngestAdminCard from '../../components/Admin/mailIngest/MailIngestAdminCard' import PlaceContribPanel from '../../components/Admin/PlaceContribPanel' import { useAdminSettingsActions } from '../../components/Admin/useAdminSettingsActions' import { placesGoogleOnlyHint } from '../../utils/placeSource' @@ -391,6 +392,28 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R )} {!managed && fileTypesCard} + + {/* Live flight status: open ADS-B data, no key, on by default; the admin may keep it off. */} + + + void handleToggleAuthSetting('flight_live_enabled', !flightLive, setFlightLive)} + /> + } + /> + + + + {/* Booking mail import: how forwarded booking mails reach this install. On a managed + install the operator runs the TREK Cloud relay, so the card is not shown there. */} + {!managed && }
@@ -517,24 +540,6 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R )} - {/* Live flight status: open ADS-B data, no key, on by default; the admin may keep it off. */} - - - void handleToggleAuthSetting('flight_live_enabled', !flightLive, setFlightLive)} - /> - } - /> - - - {/* API Keys: only the keyed providers, Google, Unsplash and Amap. They are the operator's on a managed install, so the card is not shown there (server managed.ts), and neither is the provider pick in the card diff --git a/client/src/repo/mailIngestRepo.test.ts b/client/src/repo/mailIngestRepo.test.ts new file mode 100644 index 000000000..999dc38d0 --- /dev/null +++ b/client/src/repo/mailIngestRepo.test.ts @@ -0,0 +1,38 @@ +// FE-MAILINGEST-REPO-001 to FE-MAILINGEST-REPO-003 +import { afterEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../api/mailIngest', () => ({ + mailIngestApi: { inboxCount: vi.fn(), tripAddress: vi.fn(), adminSettings: vi.fn() }, +})); + +import { mailIngestApi } from '../api/mailIngest'; +import { _resetNetworkMode, setForcedOffline } from '../sync/networkMode'; +import { mailIngestRepo } from './mailIngestRepo'; +import { OnlineOnlyError } from './onlineOnly'; + +afterEach(() => { + _resetNetworkMode(); + vi.mocked(mailIngestApi.inboxCount).mockReset(); +}); + +describe('mailIngestRepo', () => { + it('FE-MAILINGEST-REPO-001: answers parsed against the shared contract', async () => { + vi.mocked(mailIngestApi.inboxCount).mockResolvedValue({ pending: 3 }); + vi.mocked(mailIngestApi.tripAddress).mockResolvedValue({ address: null }); + expect(await mailIngestRepo.inboxCount()).toBe(3); + expect(await mailIngestRepo.tripAddress(7)).toBeNull(); + }); + + it('FE-MAILINGEST-REPO-002: an answer of another shape fails here, not in the view', async () => { + vi.mocked(mailIngestApi.inboxCount).mockResolvedValue({ pending: 'many' }); + await expect(mailIngestRepo.inboxCount()).rejects.toThrow(); + vi.mocked(mailIngestApi.adminSettings).mockResolvedValue(null); + expect(await mailIngestRepo.adminSettings()).toEqual({}); + }); + + it('FE-MAILINGEST-REPO-003: offline nothing is asked, and the caller hears why', async () => { + setForcedOffline(true); + await expect(mailIngestRepo.inboxCount()).rejects.toBeInstanceOf(OnlineOnlyError); + expect(mailIngestApi.inboxCount).not.toHaveBeenCalled(); + }); +}); diff --git a/client/src/repo/mailIngestRepo.ts b/client/src/repo/mailIngestRepo.ts new file mode 100644 index 000000000..c1c4f4cb5 --- /dev/null +++ b/client/src/repo/mailIngestRepo.ts @@ -0,0 +1,104 @@ +import { + mailIngestAddressResponseSchema, + mailIngestAdminStatusResponseSchema, + mailIngestAdminTestResponseSchema, + mailIngestCandidateResponseSchema, + mailIngestInboxCountResponseSchema, + mailIngestInboxResponseSchema, + mailIngestMessagePreviewResponseSchema, + mailIngestPrefillResponseSchema, + mailIngestSettingsResponseSchema, + mailIngestUploadResponseSchema, +} from '@trek/shared'; +import { mailIngestApi, type MailIngestAction } from '../api/mailIngest'; +import { requireOnline } from './onlineOnly'; + +/** + * The booking mail import: the person's settings and forwarding addresses, + * the inbox of bookings read from forwarded mails, the actions on them, and + * the admin's transport settings and status. + * + * **Deliberately online-only, and no Dexie table.** Every action asks the + * server to decide (which trip, whether it is a duplicate, whether the mail + * may still be undone), so there is nothing an offline copy could do. + * Answers are parsed against the shared contracts rather than cast, so a + * server that answered something else fails here, naming the field. + */ +export const mailIngestRepo = { + async settings() { + requireOnline(); + return mailIngestSettingsResponseSchema.parse(await mailIngestApi.settings()); + }, + + async updateSettings(body: { enabled?: boolean; ai_enabled?: boolean; auto_add?: boolean }) { + requireOnline(); + return mailIngestSettingsResponseSchema.parse(await mailIngestApi.updateSettings(body)); + }, + + async renewAddress() { + requireOnline(); + return mailIngestAddressResponseSchema.parse(await mailIngestApi.renewAddress()); + }, + + async tripAddress(tripId: number) { + requireOnline(); + return mailIngestAddressResponseSchema.parse(await mailIngestApi.tripAddress(tripId)).address; + }, + + async createTripAddress(tripId: number) { + requireOnline(); + return mailIngestAddressResponseSchema.parse(await mailIngestApi.createTripAddress(tripId)).address; + }, + + async inbox(before?: number) { + requireOnline(); + return mailIngestInboxResponseSchema.parse(await mailIngestApi.inbox({ status: 'open', before })); + }, + + async inboxCount() { + requireOnline(); + return mailIngestInboxCountResponseSchema.parse(await mailIngestApi.inboxCount()).pending; + }, + + async preview(messageId: number) { + requireOnline(); + return mailIngestMessagePreviewResponseSchema.parse(await mailIngestApi.preview(messageId)); + }, + + async prefill(candidateId: number) { + requireOnline(); + return mailIngestPrefillResponseSchema.parse(await mailIngestApi.prefill(candidateId)); + }, + + async act(candidateId: number, action: MailIngestAction, body: Record = {}) { + requireOnline(); + return mailIngestCandidateResponseSchema.parse(await mailIngestApi.act(candidateId, action, body)).candidate; + }, + + async upload(file: File) { + requireOnline(); + return mailIngestUploadResponseSchema.parse(await mailIngestApi.upload(file)); + }, + + async adminStatus() { + requireOnline(); + return mailIngestAdminStatusResponseSchema.parse(await mailIngestApi.adminStatus()); + }, + + async adminTest() { + requireOnline(); + return mailIngestAdminTestResponseSchema.parse(await mailIngestApi.adminTest()); + }, + + /** The admin's stored app settings (passwords masked), of which the mail form reads its own keys. */ + async adminSettings(): Promise> { + requireOnline(); + const raw = await mailIngestApi.adminSettings(); + return raw && typeof raw === 'object' ? (raw as Record) : {}; + }, + + async saveAdminSettings(patch: Record) { + requireOnline(); + await mailIngestApi.saveAdminSettings(patch); + }, +}; diff --git a/client/src/store/mailIngestStore.test.ts b/client/src/store/mailIngestStore.test.ts index 0aa00233a..2da9c890a 100644 --- a/client/src/store/mailIngestStore.test.ts +++ b/client/src/store/mailIngestStore.test.ts @@ -1,13 +1,18 @@ -// FE-STORE-MAILINGEST-001 to FE-STORE-MAILINGEST-004 +// FE-STORE-MAILINGEST-001 to FE-STORE-MAILINGEST-005 import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../api/mailIngest', () => ({ mailIngestApi: { settings: vi.fn(), inbox: vi.fn(), inboxCount: vi.fn(), act: vi.fn() }, })); vi.mock('../sync/networkMode', () => ({ isEffectivelyOffline: vi.fn(() => false) })); +const listeners = vi.hoisted(() => new Set<(event: Record) => void>()); +vi.mock('../api/websocket', () => ({ + addListener: (fn: (event: Record) => void) => listeners.add(fn), + removeListener: (fn: (event: Record) => void) => listeners.delete(fn), +})); import { mailIngestApi } from '../api/mailIngest'; -import { useMailIngestStore } from './mailIngestStore'; +import { useMailIngestStore, watchMailInbox } from './mailIngestStore'; const candidate = (id: number, resolution = 'pending') => ({ id, @@ -79,8 +84,28 @@ describe('mailIngestStore', () => { it('FE-STORE-MAILINGEST-004: an entry still open after an action stays, updated', async () => { useMailIngestStore.setState({ entries: [entry(1)] as never, pending: 1 }); vi.mocked(mailIngestApi.act).mockResolvedValue({ candidate: { ...candidate(1), reason: 'ai_provider_error' } }); - await useMailIngestStore.getState().act(1, 'read-ai'); + await useMailIngestStore.getState().act(1, 'apply'); expect(useMailIngestStore.getState().entries[0].reason).toBe('ai_provider_error'); expect(useMailIngestStore.getState().pending).toBe(1); }); + + it('FE-STORE-MAILINGEST-005: the server inbox events move the count, read the list again and reload the settings', async () => { + vi.mocked(mailIngestApi.inbox).mockResolvedValue({ entries: [entry(3)], pending: 1, next_before: null }); + vi.mocked(mailIngestApi.settings).mockResolvedValue({ nope: true }); + const stop = watchMailInbox(); + const send = (event: Record) => listeners.forEach((fn) => fn(event)); + + send({ type: 'mailInbox:changed', pending: 1 }); + expect(useMailIngestStore.getState().pending).toBe(1); + await vi.waitFor(() => expect(useMailIngestStore.getState().entries.map((e) => e.id)).toEqual([3])); + + send({ type: 'mailInbox:settings' }); + expect(mailIngestApi.settings).toHaveBeenCalled(); + + // A malformed event changes nothing, and after stopping nothing is heard. + send({ type: 'mailInbox:changed', pending: 'many' }); + expect(useMailIngestStore.getState().pending).toBe(1); + stop(); + expect(listeners.size).toBe(0); + }); }); diff --git a/client/src/store/mailIngestStore.ts b/client/src/store/mailIngestStore.ts index 39da1fd55..23f7525d8 100644 --- a/client/src/store/mailIngestStore.ts +++ b/client/src/store/mailIngestStore.ts @@ -1,14 +1,8 @@ -import { - mailIngestCandidateResponseSchema, - mailIngestInboxCountResponseSchema, - mailIngestInboxResponseSchema, - mailIngestSettingsResponseSchema, - type MailIngestCandidate, - type MailIngestInboxEntry, - type MailIngestSettingsResponse, -} from '@trek/shared'; +import type { MailIngestCandidate, MailIngestInboxEntry, MailIngestSettingsResponse } from '@trek/shared'; import { create } from 'zustand'; -import { mailIngestApi, type MailIngestAction } from '../api/mailIngest'; +import type { MailIngestAction } from '../api/mailIngest'; +import { addListener, removeListener } from '../api/websocket'; +import { mailIngestRepo } from '../repo/mailIngestRepo'; import { isEffectivelyOffline } from '../sync/networkMode'; /** @@ -43,16 +37,23 @@ export const useMailIngestStore = create((set, get) => ({ loadSettings: async () => { if (isEffectivelyOffline()) return; - const parsed = mailIngestSettingsResponseSchema.safeParse(await mailIngestApi.settings().catch(() => null)); - if (parsed.success) set({ settings: parsed.data, pending: parsed.data.pending }); + try { + const settings = await mailIngestRepo.settings(); + set({ settings, pending: settings.pending }); + } catch { + // Without an answer the menus keep what they showed; the next sign-in or event reads it again. + } }, setSettings: (settings) => set({ settings, pending: settings.pending }), loadCount: async () => { if (isEffectivelyOffline()) return; - const parsed = mailIngestInboxCountResponseSchema.safeParse(await mailIngestApi.inboxCount().catch(() => null)); - if (parsed.success) set({ pending: parsed.data.pending }); + try { + set({ pending: await mailIngestRepo.inboxCount() }); + } catch { + // The badge keeps its last count until the next event brings a new one. + } }, loadInbox: async (more = false) => { @@ -60,9 +61,7 @@ export const useMailIngestStore = create((set, get) => ({ set({ loadingInbox: true }); try { const before = more ? (get().nextBefore ?? undefined) : undefined; - const parsed = mailIngestInboxResponseSchema.safeParse(await mailIngestApi.inbox({ status: 'open', before })); - if (!parsed.success) return; - const { entries, pending, next_before } = parsed.data; + const { entries, pending, next_before } = await mailIngestRepo.inbox(before); set((state) => ({ entries: more ? [...state.entries, ...entries] : entries, pending, nextBefore: next_before })); } catch { // The inbox stays as it was; the next open reads it again. @@ -72,7 +71,7 @@ export const useMailIngestStore = create((set, get) => ({ }, act: async (candidateId, action, body = {}) => { - const { candidate } = mailIngestCandidateResponseSchema.parse(await mailIngestApi.act(candidateId, action, body)); + const candidate = await mailIngestRepo.act(candidateId, action, body); set((state) => { const stillOpen = OPEN.has(candidate.resolution); const entries = state.entries.flatMap((entry) => @@ -84,3 +83,24 @@ export const useMailIngestStore = create((set, get) => ({ return candidate; }, })); + +/** + * Keeps the inbox current while the app is open: the server says when the + * person's open count changed (a mail read, an entry decided on another + * device) and when their import settings did. The count moves at once; a + * list already read is read again, so an open inbox shows the new entry + * without a reload. Mounted once per signed-in shell (Navbar, MMailInboxHost). + */ +export function watchMailInbox(): () => void { + const onEvent = (event: Record) => { + const store = useMailIngestStore.getState(); + if (event?.type === 'mailInbox:changed' && typeof event.pending === 'number') { + useMailIngestStore.setState({ pending: event.pending }); + if (store.entries.length > 0 || event.pending > 0) void store.loadInbox(); + } else if (event?.type === 'mailInbox:settings') { + void store.loadSettings(); + } + }; + addListener(onEvent); + return () => removeListener(onEvent); +} diff --git a/client/tests/helpers/storeRegistry.ts b/client/tests/helpers/storeRegistry.ts index 24e7f4211..e66af459c 100644 --- a/client/tests/helpers/storeRegistry.ts +++ b/client/tests/helpers/storeRegistry.ts @@ -6,6 +6,7 @@ import { useAssistantHintsStore } from '../../src/store/assistantHintsStore'; import { useAuthStore } from '../../src/store/authStore'; import { useFlightLiveStore } from '../../src/store/flightLiveStore'; import { useInAppNotificationStore } from '../../src/store/inAppNotificationStore'; +import { useMailIngestStore } from '../../src/store/mailIngestStore'; import { usePaddleObstacleStore } from '../../src/store/paddleObstacleStore'; import { usePermissionsStore } from '../../src/store/permissionsStore'; import { useSettingsStore } from '../../src/store/settingsStore'; @@ -51,6 +52,8 @@ export const RESET_STORES: Record = { useAssistantHintsStore, // Live flight states are per test: a leg seeded by one test must not show up in the next. useFlightLiveStore, + // The mail inbox and its pending count, which the menus of every shell read. + useMailIngestStore, }; /** Not reset so far; moving one into RESET_STORES changes what every file that resets starts from. */ diff --git a/shared/src/i18n/ar/index.ts b/shared/src/i18n/ar/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/ar/index.ts +++ b/shared/src/i18n/ar/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/ar/mailAdmin.ts b/shared/src/i18n/ar/mailAdmin.ts new file mode 100644 index 000000000..766d597b1 --- /dev/null +++ b/shared/src/i18n/ar/mailAdmin.ts @@ -0,0 +1,45 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'استيراد رسائل الحجز', + 'mailAdmin.hint': 'تتحول رسائل تأكيد الحجز المُعاد توجيهها إلى حجوزات في الرحلة المناسبة.', + 'mailAdmin.enabled': 'استيراد رسائل الحجز', + 'mailAdmin.enabledHint': 'يمنح كل مستخدم في هذا التثبيت عنوان إعادة توجيه. عند الإيقاف، لا يستقبل أي عنوان بريداً.', + 'mailAdmin.transport.label': 'كيفية وصول البريد', + 'mailAdmin.transport.off': 'الرفع فقط', + 'mailAdmin.transport.imap': 'صندوق بريد IMAP', + 'mailAdmin.transport.webhook': 'ويب هوك', + 'mailAdmin.aiDaily': 'رسائل الذكاء الاصطناعي لكل شخص في اليوم', + 'mailAdmin.aiDailyHint': 'فوق هذا الحد، تُقرأ بيانات الحجز القابلة للقراءة آلياً فقط.', + 'mailAdmin.template': 'قالب العنوان', + 'mailAdmin.templateHint': 'عنوان إعادة التوجيه، مع {token} في موضع رمز كل شخص.', + 'mailAdmin.imap.host': 'الخادم', + 'mailAdmin.imap.port': 'المنفذ', + 'mailAdmin.imap.tls': 'التشفير', + 'mailAdmin.imap.user': 'المستخدم', + 'mailAdmin.imap.pass': 'كلمة المرور', + 'mailAdmin.imap.mailbox': 'المجلد', + 'mailAdmin.imap.mailboxHint': 'تُنقل الرسائل المقروءة إلى TREK/processed.', + 'mailAdmin.webhook.secret': 'سر التوقيع', + 'mailAdmin.webhook.secretHint': '32 حرفاً على الأقل؛ يوقّع المرسل كل رسالة به. ترك الحقل فارغاً يوقف ويب هوك.', + 'mailAdmin.inboundUrl': 'عنوان ويب هوك', + 'mailAdmin.inboundUrlHint': 'تنشر خدمة البريد لديك كل رسالة هنا، خاماً وموقّعاً.', + 'mailAdmin.copyUrl': 'نسخ العنوان', + 'mailAdmin.envHint': 'متغيرات البيئة (MAIL_INGEST_*) لها الأولوية على هذه الحقول.', + 'mailAdmin.loadError': 'تعذّر تحميل إعدادات استيراد البريد.', + 'mailAdmin.problem.secretShort': 'يجب أن يتكون السر من 32 حرفاً على الأقل.', + 'mailAdmin.problem.portInvalid': 'أدخل منفذاً بين 1 و65535.', + 'mailAdmin.problem.templateToken': 'يحتاج العنوان إلى {token} في موضع رمز كل شخص.', + 'mailAdmin.problem.dailyInvalid': 'أدخل عدداً صحيحاً من 0 إلى 1000.', + 'mailAdmin.status.title': 'الحالة', + 'mailAdmin.status.lastRead': 'آخر قراءة {time}', + 'mailAdmin.status.never': 'أبداً', + 'mailAdmin.status.mails': 'الرسائل، 24 ساعة', + 'mailAdmin.status.added': 'أُضيفت تلقائياً', + 'mailAdmin.status.waiting': 'بالانتظار', + 'mailAdmin.status.failed': 'فشلت', + 'mailAdmin.test.run': 'اختبار الاتصال', + 'mailAdmin.test.ok': 'تم الاتصال', + 'mailAdmin.test.failed': 'لا يوجد اتصال', +}; +export default mailAdmin; diff --git a/shared/src/i18n/ar/mailInbox.ts b/shared/src/i18n/ar/mailInbox.ts new file mode 100644 index 000000000..fcda84a9f --- /dev/null +++ b/shared/src/i18n/ar/mailInbox.ts @@ -0,0 +1,100 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'حجوزات قيد الانتظار', + 'mailInbox.pendingCount': '{count} حجز بانتظارك', + 'mailInbox.pendingCount.zero': '{count} حجز بانتظارك', + 'mailInbox.pendingCount.one': '{count} حجز بانتظارك', + 'mailInbox.pendingCount.two': '{count} حجزان بانتظارك', + 'mailInbox.pendingCount.few': '{count} حجوزات بانتظارك', + 'mailInbox.pendingCount.many': '{count} حجزاً بانتظارك', + 'mailInbox.empty': 'لا شيء بالانتظار', + 'mailInbox.emptyHint': 'الحجوزات التي لم يتمكن TREK من إضافتها تلقائياً تظهر هنا، مع الخيارات المتاحة لكل منها.', + 'mailInbox.untitled': 'بريد حجز', + 'mailInbox.showMail': 'عرض البريد', + 'mailInbox.backToList': 'العودة إلى القائمة', + 'mailInbox.loadMore': 'عرض المزيد', + 'mailInbox.pickTrip': 'أي رحلة؟', + 'mailInbox.added': 'تمت إضافة الحجز', + 'mailInbox.filed': 'تم الحفظ في ملفات الرحلة', + 'mailInbox.discarded': 'تم التجاهل', + 'mailInbox.actionError': 'لم تنجح العملية. حاول مرة أخرى بعد قليل.', + 'mailInbox.previewError': 'تعذّر عرض البريد.', + 'mailInbox.action.add': 'إضافة', + 'mailInbox.action.choose': 'إضافة إلى رحلة', + 'mailInbox.action.file': 'حفظ كمستند', + 'mailInbox.action.manual': 'الإدخال يدوياً', + 'mailInbox.action.discard': 'تجاهل', + 'mailInbox.reason.forwardingConfirmation': 'يطلب مزوّد بريدك تأكيد إعادة التوجيه. أدخل هذا الرمز هناك.', + 'mailInbox.reason.newsletter': 'نشرة بريدية وليست حجزاً', + 'mailInbox.reason.autoReply': 'رد تلقائي', + 'mailInbox.reason.emptyMail': 'لا يحتوي البريد على نص أو مرفق يمكن قراءته', + 'mailInbox.reason.mailUnreadable': 'تعذّرت قراءة البريد', + 'mailInbox.reason.notABooking': 'لم يُعثر على حجز في هذا البريد', + 'mailInbox.reason.tooLarge': 'البريد كبير جداً للقراءة', + 'mailInbox.reason.addressInvalid': 'وصل إلى عنوان لم يعد صالحاً', + 'mailInbox.reason.addressDailyLimit': 'بلغ عنوانك الحد اليومي من الرسائل', + 'mailInbox.reason.processingFailed': 'فشلت قراءته عدة مرات', + 'mailInbox.reason.aiQuotaExhausted': 'استُنفدت حصة الذكاء الاصطناعي لهذه الفترة', + 'mailInbox.reason.aiDisabled': 'قُرئ بدون ذكاء اصطناعي ولم يُعثر على حجز', + 'mailInbox.reason.aiUserCap': 'بلغت حدك اليومي من الرسائل المقروءة بالذكاء الاصطناعي', + 'mailInbox.reason.aiProviderError': 'تعذّرت قراءته بالذكاء الاصطناعي الآن', + 'mailInbox.reason.noTripInRange': 'لا توجد رحلة تغطي هذه التواريخ', + 'mailInbox.reason.tripAmbiguous': 'قد تكون المقصودة أكثر من رحلة', + 'mailInbox.reason.tripWithoutDates': 'ليس للرحلة تواريخ للمطابقة معها', + 'mailInbox.reason.noEditPermission': 'ليس لديك إذن بإضافة حجوزات إلى هذه الرحلة', + 'mailInbox.reason.demoMode': 'النسخة التجريبية لا تضيف حجوزات', + 'mailInbox.reason.outsideTripDates': 'هناك تاريخ خارج فترة الرحلة', + 'mailInbox.reason.sourceMismatch': 'تفاصيل البريد متعارضة فيما بينها', + 'mailInbox.reason.implausible': 'هناك قيمة لا يمكن أن تكون صحيحة', + 'mailInbox.reason.notInText': 'قيمة مقروءة غير موجودة في البريد', + 'mailInbox.reason.imageOnly': 'قُرئ من صورة فقط، لذا يحتاج إلى مراجعتك', + 'mailInbox.reason.missingCriticalField': 'النوع أو التاريخ أو المسار مفقود', + 'mailInbox.reason.missingConfirmation': 'لم يُعثر على رقم مرجع الحجز', + 'mailInbox.reason.tooManyBookings': 'عدد الحجوزات في البريد الواحد أكبر من الحد المسموح لإضافتها تلقائياً', + 'mailInbox.reason.changeOrCancellation': 'تعديل أو إلغاء لحجز سابق', + 'mailInbox.reason.cancelled': 'الحجز ملغى', + 'mailInbox.reason.possibleDuplicate': 'قد تحتوي الرحلة على هذا الحجز بالفعل', + 'mailInbox.reason.changeAvailable': 'الرحلة تحتوي على هذا الحجز، والبريد يعدّله', + 'mailInbox.reason.conflict': 'الرحلة تحتوي على هذا الحجز بتفاصيل مختلفة', + 'mailInbox.reason.askFirst': 'طلبت تأكيد كل حجز', + 'mailInbox.settings.title': 'بريد الحجوزات', + 'mailInbox.settings.on': 'مفعّل', + 'mailInbox.settings.off': 'متوقف', + 'mailInbox.settings.yourAddress': 'عنوانك', + 'mailInbox.settings.copy': 'نسخ العنوان', + 'mailInbox.settings.copyError': 'تعذّر النسخ', + 'mailInbox.settings.renew': 'عنوان جديد', + 'mailInbox.settings.renewConfirm': 'اضغط مرة أخرى: سيتوقف العنوان القديم عن العمل', + 'mailInbox.settings.renewed': 'عنوانك الجديد جاهز', + 'mailInbox.settings.saveError': 'تعذّر الحفظ', + 'mailInbox.settings.addressHint': + 'أعد توجيه رسائل تأكيد الحجز إلى هذا العنوان وسيضيفها TREK إلى الرحلة المناسبة. احتفظ به لنفسك كأنه كلمة مرور.', + 'mailInbox.settings.howTo': 'إعادة التوجيه تلقائياً', + 'mailInbox.settings.howToGmail': + 'جيميل: الإعدادات، إعادة التوجيه وPOP/IMAP، إضافة عنوان لإعادة التوجيه. يرسل جيميل رمزاً يظهر ضمن الحجوزات قيد الانتظار. بعد ذلك أضف عامل تصفية لمرسلي الحجوزات يعيد التوجيه إلى TREK.', + 'mailInbox.settings.howToOutlook': + 'أوتلوك: الإعدادات، البريد، القواعد، أضف قاعدة تعيد توجيه رسائل الحجز. بعض حسابات العمل تمنع إعادة التوجيه التلقائي؛ أعد التوجيه يدوياً في هذه الحالة.', + 'mailInbox.settings.howToManual': 'يدوياً: أعد توجيه أي رسالة حجز مع مرفقاتها.', + 'mailInbox.settings.enabled': 'استيراد رسائل الحجز', + 'mailInbox.settings.enabledHint': 'عند الإيقاف، لا تُقرأ الرسائل الواردة إلى عنوانك.', + 'mailInbox.settings.ai': 'القراءة بالذكاء الاصطناعي', + 'mailInbox.settings.aiHint': + 'يقرأ أي رسالة حجز ويحدد رحلتها. يُرسل البريد وتواريخ رحلاتك ووجهاتها إلى مزوّد الذكاء الاصطناعي.', + 'mailInbox.settings.aiUnavailable': 'غير متاح هنا: تُقرأ فقط الرسائل التي تحتوي على بيانات حجز قابلة للقراءة آلياً.', + 'mailInbox.settings.autoAdd': 'الإضافة تلقائياً', + 'mailInbox.settings.autoAddHint': + 'الحجز الذي يتأكد منه TREK يُضاف مباشرة إلى رحلته، ويمكنك التراجع عنه خلال 7 أيام. عند الإيقاف، ينتظر كل حجز موافقتك.', + 'mailInbox.settings.upload': 'رفع ملف بريد', + 'mailInbox.settings.uploaded': 'تم استلام البريد وجارٍ قراءته', + 'mailInbox.settings.uploadDuplicate': 'هذا البريد مضاف بالفعل', + 'mailInbox.settings.uploadError': 'تعذّرت قراءة الملف كرسالة بريد', + 'mailInbox.settings.privacy': + 'تُحفظ الرسائل المُعاد توجيهها مشفّرة لمدة 30 يوماً كحد أقصى ثم تُحذف. يُرجى عدم إعادة توجيه وثائق الهوية أو المستندات الصحية.', + 'mailInbox.trip.title': 'أو أعد التوجيه بالبريد', + 'mailInbox.trip.hint': 'رسالة الحجز المرسلة إلى عنوان هذه الرحلة تصل إلى هنا دون أي تخمين.', + 'mailInbox.trip.create': 'إنشاء عنوان لهذه الرحلة', + 'mailInbox.fromMail': 'من بريد', + 'mailInbox.fromMailHint': 'أُضيف من رسالة حجز مُعاد توجيهها', +}; +export default mailInbox; diff --git a/shared/src/i18n/ar/notif.ts b/shared/src/i18n/ar/notif.ts index ae249b728..4d628dd93 100644 --- a/shared/src/i18n/ar/notif.ts +++ b/shared/src/i18n/ar/notif.ts @@ -63,7 +63,12 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': 'تمت إضافة {booking} إلى {trip}', 'notif.mail_booking_added.shared': 'أضاف {actor} الحجز {booking} إلى {trip} من بريد مُعاد توجيهه', 'notif.mail_booking_pending.title': 'حجوزات مُعاد توجيهها للمراجعة', - 'notif.mail_booking_pending.text': 'حجوزات مُعاد توجيهها بانتظارك: {count}', + 'notif.mail_booking_pending.text': '{count} حجز مُعاد توجيهه بانتظارك', + 'notif.mail_booking_pending.text.zero': '{count} حجز مُعاد توجيهه بانتظارك', + 'notif.mail_booking_pending.text.one': '{count} حجز مُعاد توجيهه بانتظارك', + 'notif.mail_booking_pending.text.two': '{count} حجزان مُعاد توجيههما بانتظارك', + 'notif.mail_booking_pending.text.few': '{count} حجوزات مُعاد توجيهها بانتظارك', + 'notif.mail_booking_pending.text.many': '{count} حجزاً مُعاد توجيهه بانتظارك', 'notif.mail_ingest_paused.title': 'تم إيقاف استيراد البريد مؤقتًا', 'notif.mail_ingest_paused.text': 'لا تُقبل رسائل الحجز المُعاد توجيهها في الوقت الحالي. تبقى عناوينك وستعمل مجددًا عند عودة الاستيراد.', diff --git a/shared/src/i18n/ar/reservations.ts b/shared/src/i18n/ar/reservations.ts index 3ab6a9797..ebe34e09b 100644 --- a/shared/src/i18n/ar/reservations.ts +++ b/shared/src/i18n/ar/reservations.ts @@ -35,7 +35,10 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'رحلة متّصلة', 'reservations.layover.layover': 'توقف بيني', 'reservations.needsReview': 'مراجعة', - 'reservations.needsReviewHint': 'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.', + 'reservations.needsReviewHint': 'عُبّئت بعض التفاصيل تلقائياً وتعذّرت قراءتها بشكل مؤكد. يُرجى التحقق منها.', + 'reservations.reviewConfirm': 'تبدو صحيحة', + 'reservations.reviewConfirmHint': 'اضغط لتحديدها كمراجَعة.', + 'reservations.reviewConfirmed': 'تم تحديدها كمراجَعة', 'reservations.searchLocation': 'ابحث عن محطة، ميناء، عنوان...', 'reservations.meta.trainNumber': 'رقم القطار', 'reservations.meta.platform': 'المنصة', diff --git a/shared/src/i18n/ar/settings.ts b/shared/src/i18n/ar/settings.ts index ebecdf98e..7d2a5cc3a 100644 --- a/shared/src/i18n/ar/settings.ts +++ b/shared/src/i18n/ar/settings.ts @@ -110,6 +110,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'تأخر الرحلات', 'settings.notifyFlightLanded': 'هبوط الرحلات', 'settings.notifyTripReminder': 'تذكيرات الرحلات', + 'settings.notifyMailBookingAdded': 'حجوزات أُضيفت من رسائل البريد', + 'settings.notifyMailBookingPending': 'حجوزات مُعاد توجيهها للمراجعة', + 'settings.notifyMailIngestPaused': 'تم إيقاف استيراد البريد مؤقتاً', 'settings.notifyTodoDue': 'مهمة مستحقة', 'settings.notifyVacayInvite': 'دعوات دمج الإجازات', 'settings.notifyVacayShare': 'مشاركات تقويم Vacay', diff --git a/shared/src/i18n/az/index.ts b/shared/src/i18n/az/index.ts index bb9ce471c..74f0d5974 100644 --- a/shared/src/i18n/az/index.ts +++ b/shared/src/i18n/az/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/az/mailAdmin.ts b/shared/src/i18n/az/mailAdmin.ts new file mode 100644 index 000000000..b1c352cc9 --- /dev/null +++ b/shared/src/i18n/az/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Rezervasiya məktublarının idxalı', + 'mailAdmin.hint': 'Yönləndirilən rezervasiya təsdiqləri düzgün səyahətdə rezervasiyaya çevrilir.', + 'mailAdmin.enabled': 'Rezervasiya məktublarını idxal et', + 'mailAdmin.enabledHint': + 'Bu quraşdırmadakı hər kəsə yönləndirmə ünvanı verir. Deaktiv olduqda heç bir ünvan məktub qəbul etmir.', + 'mailAdmin.transport.label': 'Məktub necə gəlir', + 'mailAdmin.transport.off': 'Yalnız yükləmə', + 'mailAdmin.transport.imap': 'IMAP poçt qutusu', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Adambaşı gündəlik süni intellekt məktubu', + 'mailAdmin.aiDailyHint': 'Bundan yuxarıda yalnız maşın tərəfindən oxuna bilən rezervasiya məlumatı oxunur.', + 'mailAdmin.template': 'Ünvan şablonu', + 'mailAdmin.templateHint': 'Yönləndirmə ünvanı; hər şəxsin kodu gələn yerdə {token} olur.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Şifrələmə', + 'mailAdmin.imap.user': 'İstifadəçi', + 'mailAdmin.imap.pass': 'Parol', + 'mailAdmin.imap.mailbox': 'Qovluq', + 'mailAdmin.imap.mailboxHint': 'Oxunan məktublar TREK/processed qovluğuna köçürülür.', + 'mailAdmin.webhook.secret': 'İmza açarı', + 'mailAdmin.webhook.secretHint': + 'Ən azı 32 simvol; göndərən hər məktubu bununla imzalayır. Boş buraxılsa webhook söndürülür.', + 'mailAdmin.inboundUrl': 'Webhook ünvanı', + 'mailAdmin.inboundUrlHint': 'Poçt xidmətin hər məktubu xam və imzalı şəkildə bura göndərir.', + 'mailAdmin.copyUrl': 'Ünvanı kopyala', + 'mailAdmin.envHint': 'Mühit dəyişənləri (MAIL_INGEST_*) bu sahələrdən üstündür.', + 'mailAdmin.loadError': 'Məktub idxalı ayarları yüklənmədi.', + 'mailAdmin.problem.secretShort': 'Açar ən azı 32 simvol olmalıdır.', + 'mailAdmin.problem.portInvalid': '1 ilə 65535 arasında port daxil et.', + 'mailAdmin.problem.templateToken': 'Ünvanda hər şəxsin kodu üçün {token} olmalıdır.', + 'mailAdmin.problem.dailyInvalid': '0 ilə 1000 arasında tam ədəd daxil et.', + 'mailAdmin.status.title': 'Status', // same-as-en + 'mailAdmin.status.lastRead': 'Son oxuma {time}', + 'mailAdmin.status.never': 'heç vaxt', + 'mailAdmin.status.mails': 'Məktublar, 24 saat', + 'mailAdmin.status.added': 'Özbaşına əlavə edilən', + 'mailAdmin.status.waiting': 'Gözləyən', + 'mailAdmin.status.failed': 'Uğursuz', + 'mailAdmin.test.run': 'Bağlantını yoxla', + 'mailAdmin.test.ok': 'Qoşuldu', + 'mailAdmin.test.failed': 'Bağlantı yoxdur', +}; +export default mailAdmin; diff --git a/shared/src/i18n/az/mailInbox.ts b/shared/src/i18n/az/mailInbox.ts new file mode 100644 index 000000000..9b6511d4a --- /dev/null +++ b/shared/src/i18n/az/mailInbox.ts @@ -0,0 +1,99 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Gözləyən rezervasiyalar', + 'mailInbox.pendingCount': '{count} rezervasiya səni gözləyir', + 'mailInbox.pendingCount.one': '{count} rezervasiya səni gözləyir', + 'mailInbox.empty': 'Gözləyən heç nə yoxdur', + 'mailInbox.emptyHint': + 'TREK-in özbaşına əlavə edə bilmədiyi rezervasiyalar burada görünür, hər biri üçün edə biləcəklərinlə birlikdə.', + 'mailInbox.untitled': 'Rezervasiya məktubu', + 'mailInbox.showMail': 'Məktubu göstər', + 'mailInbox.backToList': 'Siyahıya qayıt', + 'mailInbox.loadMore': 'Daha çox göstər', + 'mailInbox.pickTrip': 'Hansı səyahət?', + 'mailInbox.added': 'Rezervasiya əlavə edildi', + 'mailInbox.filed': 'Səyahətin fayllarına saxlanıldı', + 'mailInbox.discarded': 'Silindi', + 'mailInbox.actionError': 'Əməliyyat alınmadı. Bir azdan yenidən yoxla.', + 'mailInbox.previewError': 'Məktub göstərilə bilmədi.', + 'mailInbox.action.add': 'Əlavə et', + 'mailInbox.action.choose': 'Səyahətə əlavə et', + 'mailInbox.action.file': 'Sənəd kimi saxla', + 'mailInbox.action.manual': 'Özün doldur', + 'mailInbox.action.discard': 'Sil', + 'mailInbox.reason.forwardingConfirmation': + 'Poçt provayderin yönləndirməni təsdiqləməyi istəyir. Bu kodu orada daxil et.', + 'mailInbox.reason.newsletter': 'Xəbər bülleteni, rezervasiya deyil', + 'mailInbox.reason.autoReply': 'Avtomatik cavab', + 'mailInbox.reason.emptyMail': 'Məktubda oxunacaq mətn və ya əlavə yoxdur', + 'mailInbox.reason.mailUnreadable': 'Məktub oxuna bilmədi', + 'mailInbox.reason.notABooking': 'Bu məktubda rezervasiya tapılmadı', + 'mailInbox.reason.tooLarge': 'Məktub oxunmaq üçün çox böyükdür', + 'mailInbox.reason.addressInvalid': 'Artıq işləməyən bir ünvana gəlib', + 'mailInbox.reason.addressDailyLimit': 'Ünvanın bugünkü məktub limitinə çatıb', + 'mailInbox.reason.processingFailed': 'Oxuma bir neçə dəfə uğursuz oldu', + 'mailInbox.reason.aiQuotaExhausted': 'Bu dövrün süni intellekt limiti bitib', + 'mailInbox.reason.aiDisabled': 'Süni intellektsiz oxundu, rezervasiya tapılmadı', + 'mailInbox.reason.aiUserCap': 'Süni intellektlə oxunan məktublar üçün gündəlik limitinə çatdın', + 'mailInbox.reason.aiProviderError': 'Süni intellekt hazırda oxuya bilmədi', + 'mailInbox.reason.noTripInRange': 'Bu tarixləri əhatə edən səyahət yoxdur', + 'mailInbox.reason.tripAmbiguous': 'Birdən çox səyahət nəzərdə tutulmuş ola bilər', + 'mailInbox.reason.tripWithoutDates': 'Səyahətin müqayisə üçün tarixi yoxdur', + 'mailInbox.reason.noEditPermission': 'Bu səyahətə rezervasiya əlavə etmək icazən yoxdur', + 'mailInbox.reason.demoMode': 'Demo rezervasiya əlavə etmir', + 'mailInbox.reason.outsideTripDates': 'Bir tarix səyahətdən kənardadır', + 'mailInbox.reason.sourceMismatch': 'Məktubdakı məlumatlar bir-biri ilə uyğun gəlmir', + 'mailInbox.reason.implausible': 'Bir dəyər düzgün ola bilməz', + 'mailInbox.reason.notInText': 'Oxunan bir dəyər məktubda yoxdur', + 'mailInbox.reason.imageOnly': 'Yalnız şəkildən oxundu, ona görə baxmalısan', + 'mailInbox.reason.missingCriticalField': 'Növ, tarix və ya marşrut çatışmır', + 'mailInbox.reason.missingConfirmation': 'Rezervasiya nömrəsi tapılmadı', + 'mailInbox.reason.tooManyBookings': 'Bir məktubda özbaşına əlavə edilənlərdən çox rezervasiya var', + 'mailInbox.reason.changeOrCancellation': 'Əvvəlki rezervasiyanın dəyişikliyi və ya ləğvi', + 'mailInbox.reason.cancelled': 'Rezervasiya ləğv edilib', + 'mailInbox.reason.possibleDuplicate': 'Səyahətdə bu rezervasiya artıq ola bilər', + 'mailInbox.reason.changeAvailable': 'Səyahətdə bu rezervasiya var, məktub onu dəyişir', + 'mailInbox.reason.conflict': 'Səyahətdə bu rezervasiya fərqli məlumatlarla var', + 'mailInbox.reason.askFirst': 'Hər rezervasiyanı təsdiqləməyi istəmisən', + 'mailInbox.settings.title': 'Rezervasiya məktubları', + 'mailInbox.settings.on': 'Aktiv', + 'mailInbox.settings.off': 'Deaktiv', + 'mailInbox.settings.yourAddress': 'Ünvanın', + 'mailInbox.settings.copy': 'Ünvanı kopyala', + 'mailInbox.settings.copyError': 'Kopyalanmadı', + 'mailInbox.settings.renew': 'Yeni ünvan', + 'mailInbox.settings.renewConfirm': 'Yenidən toxun: köhnə ünvan işləməyəcək', + 'mailInbox.settings.renewed': 'Yeni ünvanın hazırdır', + 'mailInbox.settings.saveError': 'Yadda saxlanmadı', + 'mailInbox.settings.addressHint': + 'Rezervasiya təsdiqlərini bu ünvana yönləndir, TREK onları düzgün səyahətə əlavə edəcək. Parol kimi onu özündə saxla.', + 'mailInbox.settings.howTo': 'Avtomatik yönləndir', + 'mailInbox.settings.howToGmail': + 'Gmail: Ayarlar, Yönləndirmə və POP/IMAP, Yönləndirmə ünvanı əlavə et. Gmail kod göndərir, kod Gözləyən rezervasiyalar altında görünür. Sonra rezervasiya göndərənləri üçün TREK-ə yönləndirən filtr əlavə et.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Ayarlar, Poçt, Qaydalar, rezervasiya məktublarını yönləndirən qayda əlavə et. Bəzi iş hesabları avtomatik yönləndirməni bloklayır; orada əllə yönləndir.', + 'mailInbox.settings.howToManual': 'Əllə: istənilən rezervasiya məktubunu əlavələri ilə birlikdə yönləndir.', + 'mailInbox.settings.enabled': 'Rezervasiya məktublarını idxal et', + 'mailInbox.settings.enabledHint': 'Deaktiv olduqda ünvanına gələn məktublar oxunmur.', + 'mailInbox.settings.ai': 'Süni intellektlə oxu', + 'mailInbox.settings.aiHint': + 'İstənilən rezervasiya məktubunu oxuyur və səyahətini tapır. Məktub, səyahətlərinin tarixləri və təyinat məntəqələri süni intellekt provayderinə göndərilir.', + 'mailInbox.settings.aiUnavailable': + 'Burada əlçatan deyil: yalnız maşın tərəfindən oxuna bilən rezervasiya məlumatı olan məktublar oxunur.', + 'mailInbox.settings.autoAdd': 'Özbaşına əlavə et', + 'mailInbox.settings.autoAddHint': + 'TREK-in əmin olduğu rezervasiya birbaşa səyahətinə düşür və 7 gün ərzində geri ala bilərsən. Deaktiv olduqda hər biri təsdiqini gözləyir.', + 'mailInbox.settings.upload': 'Məktub faylı yüklə', + 'mailInbox.settings.uploaded': 'Məktub qəbul edildi, oxunur', + 'mailInbox.settings.uploadDuplicate': 'Bu məktub artıq əlavə edilib', + 'mailInbox.settings.uploadError': 'Fayl məktub kimi oxuna bilmədi', + 'mailInbox.settings.privacy': + 'Yönləndirilən məktublar ən çox 30 gün şifrələnmiş saxlanılır, sonra silinir. Xahiş edirik, şəxsiyyət və ya sağlamlıq sənədlərini yönləndirmə.', + 'mailInbox.trip.title': 'Və ya məktubla yönləndir', + 'mailInbox.trip.hint': 'Bu səyahətin ünvanına göndərilən rezervasiya məktubu təxmin edilmədən birbaşa bura düşür.', + 'mailInbox.trip.create': 'Bu səyahət üçün ünvan yarat', + 'mailInbox.fromMail': 'Məktubdan', + 'mailInbox.fromMailHint': 'Yönləndirilmiş rezervasiya məktubundan əlavə edilib', +}; +export default mailInbox; diff --git a/shared/src/i18n/az/notif.ts b/shared/src/i18n/az/notif.ts index 15a16aaf3..631d07698 100644 --- a/shared/src/i18n/az/notif.ts +++ b/shared/src/i18n/az/notif.ts @@ -60,7 +60,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.shared': '{actor} yönləndirilmiş məktubdan {booking} rezervasiyasını {trip} səyahətinə əlavə etdi', 'notif.mail_booking_pending.title': 'Yoxlanılacaq yönləndirilmiş rezervasiyalar', - 'notif.mail_booking_pending.text': 'Sizi gözləyən yönləndirilmiş rezervasiyalar: {count}', + 'notif.mail_booking_pending.text': '{count} yönləndirilmiş rezervasiya səni gözləyir', + 'notif.mail_booking_pending.text.one': '{count} yönləndirilmiş rezervasiya səni gözləyir', 'notif.mail_ingest_paused.title': 'Poçt idxalı dayandırılıb', 'notif.mail_ingest_paused.text': 'Yönləndirilmiş rezervasiya məktubları hələlik qəbul edilmir. Ünvanlarınız qalır və idxal geri qayıdanda yenidən işləyəcək.', diff --git a/shared/src/i18n/az/reservations.ts b/shared/src/i18n/az/reservations.ts index 6c41e9407..eefee17c5 100644 --- a/shared/src/i18n/az/reservations.ts +++ b/shared/src/i18n/az/reservations.ts @@ -43,7 +43,10 @@ const reservations: TranslationStrings = { 'reservations.layover.layover': 'Tranzit gözləmə', 'reservations.needsReview': 'Yoxlayın', - 'reservations.needsReviewHint': 'Hava limanını avtomatik uyğunlaşdırmaq mümkün olmadı — məkanı təsdiqləyin.', + 'reservations.needsReviewHint': 'Bəzi məlumatlar avtomatik dolduruldu və dəqiq oxuna bilmədi. Xahiş edirik, yoxla.', + 'reservations.reviewConfirm': 'Düzgün görünür', + 'reservations.reviewConfirmHint': 'Yoxlanıldı kimi işarələmək üçün klik et.', + 'reservations.reviewConfirmed': 'Yoxlanıldı kimi işarələndi', 'reservations.searchLocation': 'Stansiya, liman və ya ünvan axtarın…', 'reservations.meta.trainNumber': 'Qatar nömrəsi', diff --git a/shared/src/i18n/az/settings.ts b/shared/src/i18n/az/settings.ts index 5eb3a68cd..d05d8c967 100644 --- a/shared/src/i18n/az/settings.ts +++ b/shared/src/i18n/az/settings.ts @@ -145,6 +145,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Reyslərin gecikməsi', 'settings.notifyFlightLanded': 'Reyslərin enişi', 'settings.notifyTripReminder': 'Səyahət xatırlatmaları', + 'settings.notifyMailBookingAdded': 'Məktublardan əlavə edilən rezervasiyalar', + 'settings.notifyMailBookingPending': 'Yoxlanılacaq yönləndirilmiş rezervasiyalar', + 'settings.notifyMailIngestPaused': 'Məktub idxalı dayandırıldı', 'settings.notifyTodoDue': 'Son tarixi yaxınlaşan tapşırıqlar', 'settings.notifyVacayInvite': 'Vacay birləşdirmə dəvətləri', 'settings.notifyVacayShare': 'Vacay təqvim paylaşımları', diff --git a/shared/src/i18n/br/index.ts b/shared/src/i18n/br/index.ts index 2a7b65d09..2a9d5857e 100644 --- a/shared/src/i18n/br/index.ts +++ b/shared/src/i18n/br/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/br/mailAdmin.ts b/shared/src/i18n/br/mailAdmin.ts new file mode 100644 index 000000000..e8412f55e --- /dev/null +++ b/shared/src/i18n/br/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Importação de e-mails de reserva', + 'mailAdmin.hint': 'As confirmações de reserva encaminhadas viram reservas na viagem certa.', + 'mailAdmin.enabled': 'Importar e-mails de reserva', + 'mailAdmin.enabledHint': + 'Dá a todos nesta instalação um endereço de encaminhamento. Desativado, nenhum endereço recebe e-mails.', + 'mailAdmin.transport.label': 'Como o e-mail chega', + 'mailAdmin.transport.off': 'Somente envio de arquivo', + 'mailAdmin.transport.imap': 'Caixa de correio IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'E-mails por IA por pessoa e dia', + 'mailAdmin.aiDailyHint': 'Acima disso, só dados de reserva legíveis por máquina são lidos.', + 'mailAdmin.template': 'Modelo de endereço', + 'mailAdmin.templateHint': 'O endereço de encaminhamento, com {token} no lugar do código de cada pessoa.', + 'mailAdmin.imap.host': 'Servidor', + 'mailAdmin.imap.port': 'Porta', + 'mailAdmin.imap.tls': 'Criptografia', + 'mailAdmin.imap.user': 'Usuário', + 'mailAdmin.imap.pass': 'Senha', + 'mailAdmin.imap.mailbox': 'Pasta', + 'mailAdmin.imap.mailboxHint': 'Os e-mails lidos são movidos para TREK/processed.', + 'mailAdmin.webhook.secret': 'Segredo de assinatura', + 'mailAdmin.webhook.secretHint': + 'Pelo menos 32 caracteres; o remetente assina cada e-mail com ele. Vazio, o webhook fica desativado.', + 'mailAdmin.inboundUrl': 'Endereço do webhook', + 'mailAdmin.inboundUrlHint': 'Seu serviço de e-mail envia cada e-mail para cá, bruto e assinado.', + 'mailAdmin.copyUrl': 'Copiar endereço', + 'mailAdmin.envHint': 'As variáveis de ambiente (MAIL_INGEST_*) têm prioridade sobre estes campos.', + 'mailAdmin.loadError': 'Não foi possível carregar as configurações de importação de e-mails.', + 'mailAdmin.problem.secretShort': 'O segredo precisa ter pelo menos 32 caracteres.', + 'mailAdmin.problem.portInvalid': 'Digite uma porta entre 1 e 65535.', + 'mailAdmin.problem.templateToken': 'O endereço precisa de {token} para o código de cada pessoa.', + 'mailAdmin.problem.dailyInvalid': 'Digite um número inteiro de 0 a 1000.', + 'mailAdmin.status.title': 'Status', // same-as-en + 'mailAdmin.status.lastRead': 'Última leitura {time}', + 'mailAdmin.status.never': 'nunca', + 'mailAdmin.status.mails': 'E-mails, 24 h', + 'mailAdmin.status.added': 'Adicionados automaticamente', + 'mailAdmin.status.waiting': 'Aguardando', + 'mailAdmin.status.failed': 'Com falha', + 'mailAdmin.test.run': 'Testar conexão', + 'mailAdmin.test.ok': 'Conectado', + 'mailAdmin.test.failed': 'Sem conexão', +}; +export default mailAdmin; diff --git a/shared/src/i18n/br/mailInbox.ts b/shared/src/i18n/br/mailInbox.ts new file mode 100644 index 000000000..73a43371b --- /dev/null +++ b/shared/src/i18n/br/mailInbox.ts @@ -0,0 +1,99 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Reservas pendentes', + 'mailInbox.pendingCount': '{count} reservas esperam por você', + 'mailInbox.pendingCount.one': '{count} reserva espera por você', + 'mailInbox.empty': 'Nada pendente', + 'mailInbox.emptyHint': + 'As reservas que o TREK não conseguiu adicionar sozinho aparecem aqui, com o que você pode fazer em cada caso.', + 'mailInbox.untitled': 'E-mail de reserva', + 'mailInbox.showMail': 'Mostrar e-mail', + 'mailInbox.backToList': 'Voltar à lista', + 'mailInbox.loadMore': 'Mostrar mais', + 'mailInbox.pickTrip': 'Qual viagem?', + 'mailInbox.added': 'Reserva adicionada', + 'mailInbox.filed': 'Salvo nos arquivos da viagem', + 'mailInbox.discarded': 'Descartado', + 'mailInbox.actionError': 'Não deu certo. Tente de novo em instantes.', + 'mailInbox.previewError': 'Não foi possível mostrar o e-mail.', + 'mailInbox.action.add': 'Adicionar', + 'mailInbox.action.choose': 'Adicionar a uma viagem', + 'mailInbox.action.file': 'Salvar como documento', + 'mailInbox.action.manual': 'Preencher você mesmo', + 'mailInbox.action.discard': 'Descartar', + 'mailInbox.reason.forwardingConfirmation': + 'Seu provedor de e-mail pede para confirmar o encaminhamento. Digite este código lá.', + 'mailInbox.reason.newsletter': 'Uma newsletter, não uma reserva', + 'mailInbox.reason.autoReply': 'Uma resposta automática', + 'mailInbox.reason.emptyMail': 'O e-mail não tem texto nem anexo para ler', + 'mailInbox.reason.mailUnreadable': 'Não foi possível ler o e-mail', + 'mailInbox.reason.notABooking': 'Nenhuma reserva encontrada neste e-mail', + 'mailInbox.reason.tooLarge': 'O e-mail é grande demais para ser lido', + 'mailInbox.reason.addressInvalid': 'Chegou a um endereço que não funciona mais', + 'mailInbox.reason.addressDailyLimit': 'Seu endereço já recebeu todos os e-mails de hoje', + 'mailInbox.reason.processingFailed': 'A leitura falhou várias vezes', + 'mailInbox.reason.aiQuotaExhausted': 'A cota de IA deste período acabou', + 'mailInbox.reason.aiDisabled': 'Lido sem IA, que não encontrou nenhuma reserva', + 'mailInbox.reason.aiUserCap': 'Você atingiu o limite diário de e-mails lidos por IA', + 'mailInbox.reason.aiProviderError': 'A IA não conseguiu ler agora', + 'mailInbox.reason.noTripInRange': 'Nenhuma viagem cobre essas datas', + 'mailInbox.reason.tripAmbiguous': 'Pode ser mais de uma viagem', + 'mailInbox.reason.tripWithoutDates': 'A viagem não tem datas para comparar', + 'mailInbox.reason.noEditPermission': 'Você não pode adicionar reservas a essa viagem', + 'mailInbox.reason.demoMode': 'A demonstração não adiciona reservas', + 'mailInbox.reason.outsideTripDates': 'Uma data está fora da viagem', + 'mailInbox.reason.sourceMismatch': 'Os dados do e-mail não batem entre si', + 'mailInbox.reason.implausible': 'Um valor não pode estar certo', + 'mailInbox.reason.notInText': 'Um valor lido não consta no e-mail', + 'mailInbox.reason.imageOnly': 'Lido só a partir de uma imagem, então precisa da sua conferência', + 'mailInbox.reason.missingCriticalField': 'Faltam o tipo, a data ou o trajeto', + 'mailInbox.reason.missingConfirmation': 'Nenhum código de reserva encontrado', + 'mailInbox.reason.tooManyBookings': 'Há mais reservas em um só e-mail do que o limite para adicionar sozinho', + 'mailInbox.reason.changeOrCancellation': 'Uma alteração ou cancelamento de uma reserva anterior', + 'mailInbox.reason.cancelled': 'A reserva foi cancelada', + 'mailInbox.reason.possibleDuplicate': 'A viagem talvez já tenha essa reserva', + 'mailInbox.reason.changeAvailable': 'A viagem já tem essa reserva, e o e-mail a altera', + 'mailInbox.reason.conflict': 'A viagem tem essa reserva com outros dados', + 'mailInbox.reason.askFirst': 'Você pediu para confirmar cada reserva', + 'mailInbox.settings.title': 'E-mails de reserva', + 'mailInbox.settings.on': 'Ativado', + 'mailInbox.settings.off': 'Desativado', + 'mailInbox.settings.yourAddress': 'Seu endereço', + 'mailInbox.settings.copy': 'Copiar endereço', + 'mailInbox.settings.copyError': 'Não foi possível copiar', + 'mailInbox.settings.renew': 'Novo endereço', + 'mailInbox.settings.renewConfirm': 'Toque de novo: o endereço antigo deixa de funcionar', + 'mailInbox.settings.renewed': 'Seu novo endereço está pronto', + 'mailInbox.settings.saveError': 'Não foi possível salvar', + 'mailInbox.settings.addressHint': + 'Encaminhe as confirmações de reserva para este endereço e o TREK as adiciona à viagem certa. Mantenha-o em sigilo, como uma senha.', + 'mailInbox.settings.howTo': 'Encaminhar automaticamente', + 'mailInbox.settings.howToGmail': + 'Gmail: Configurações, Encaminhamento e POP/IMAP, Adicionar um endereço de encaminhamento. O Gmail envia um código, que aparece em Reservas pendentes. Depois, crie um filtro para os remetentes de reservas que encaminhe ao TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Configurações, E-mail, Regras, crie uma regra que encaminhe os e-mails de reserva. Algumas contas corporativas bloqueiam o encaminhamento automático; nesses casos, encaminhe manualmente.', + 'mailInbox.settings.howToManual': 'Manualmente: encaminhe qualquer e-mail de reserva, com os anexos.', + 'mailInbox.settings.enabled': 'Importar e-mails de reserva', + 'mailInbox.settings.enabledHint': 'Desativado, os e-mails enviados ao seu endereço não são lidos.', + 'mailInbox.settings.ai': 'Ler com IA', + 'mailInbox.settings.aiHint': + 'Lê qualquer e-mail de reserva e descobre a viagem correspondente. O e-mail e as datas e destinos das suas viagens são enviados ao provedor de IA.', + 'mailInbox.settings.aiUnavailable': + 'Indisponível aqui: só são lidos e-mails com dados de reserva legíveis por máquina.', + 'mailInbox.settings.autoAdd': 'Adicionar automaticamente', + 'mailInbox.settings.autoAddHint': + 'Uma reserva da qual o TREK tem certeza vai direto para a viagem, e você pode desfazer por 7 dias. Desativado, cada uma espera o seu OK.', + 'mailInbox.settings.upload': 'Enviar um arquivo de e-mail', + 'mailInbox.settings.uploaded': 'E-mail recebido, está sendo lido', + 'mailInbox.settings.uploadDuplicate': 'Este e-mail já foi importado', + 'mailInbox.settings.uploadError': 'Não foi possível ler o arquivo como e-mail', + 'mailInbox.settings.privacy': + 'Os e-mails encaminhados ficam armazenados criptografados por no máximo 30 dias e depois são excluídos. Não encaminhe documentos de identidade nem de saúde.', + 'mailInbox.trip.title': 'Ou encaminhe por e-mail', + 'mailInbox.trip.hint': 'Um e-mail de reserva enviado ao endereço desta viagem chega aqui sem adivinhação.', + 'mailInbox.trip.create': 'Criar um endereço para esta viagem', + 'mailInbox.fromMail': 'Do e-mail', + 'mailInbox.fromMailHint': 'Adicionado a partir de um e-mail de reserva encaminhado', +}; +export default mailInbox; diff --git a/shared/src/i18n/br/notif.ts b/shared/src/i18n/br/notif.ts index 9135bb3e0..15d92ca47 100644 --- a/shared/src/i18n/br/notif.ts +++ b/shared/src/i18n/br/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} foi adicionada a {trip}', 'notif.mail_booking_added.shared': '{actor} adicionou {booking} a {trip} a partir de um e-mail encaminhado', 'notif.mail_booking_pending.title': 'Reservas encaminhadas para revisar', - 'notif.mail_booking_pending.text': 'Reservas encaminhadas aguardando você: {count}', + 'notif.mail_booking_pending.text': '{count} reservas encaminhadas esperam por você', + 'notif.mail_booking_pending.text.one': '{count} reserva encaminhada espera por você', 'notif.mail_ingest_paused.title': 'Importação de e-mails pausada', 'notif.mail_ingest_paused.text': 'Por enquanto, os e-mails de reserva encaminhados não são aceitos. Seus endereços continuam e voltam a funcionar quando a importação voltar.', diff --git a/shared/src/i18n/br/reservations.ts b/shared/src/i18n/br/reservations.ts index 1e9d66ad1..b121872bc 100644 --- a/shared/src/i18n/br/reservations.ts +++ b/shared/src/i18n/br/reservations.ts @@ -35,7 +35,11 @@ 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': + 'Alguns dados foram preenchidos automaticamente e não puderam ser lidos com certeza. Confira-os.', + 'reservations.reviewConfirm': 'Está certo', + 'reservations.reviewConfirmHint': 'Clique para marcar como conferido.', + 'reservations.reviewConfirmed': 'Marcado como conferido', 'reservations.searchLocation': 'Buscar estação, porto, endereço...', 'reservations.meta.trainNumber': 'Nº do trem', 'reservations.meta.platform': 'Plataforma', diff --git a/shared/src/i18n/br/settings.ts b/shared/src/i18n/br/settings.ts index 721110dc6..73e27e8c0 100644 --- a/shared/src/i18n/br/settings.ts +++ b/shared/src/i18n/br/settings.ts @@ -117,6 +117,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Atrasos de voos', 'settings.notifyFlightLanded': 'Pousos', 'settings.notifyTripReminder': 'Lembretes de viagem', + 'settings.notifyMailBookingAdded': 'Reservas adicionadas por e-mail', + 'settings.notifyMailBookingPending': 'Reservas encaminhadas para revisar', + 'settings.notifyMailIngestPaused': 'Importação de e-mails pausada', 'settings.notifyTodoDue': 'Tarefa com vencimento', 'settings.notifyVacayInvite': 'Convites de fusão Vacay', 'settings.notifyVacayShare': 'Compartilhamentos de calendário Vacay', diff --git a/shared/src/i18n/ca/index.ts b/shared/src/i18n/ca/index.ts index 8b23911a3..2471bf6ac 100644 --- a/shared/src/i18n/ca/index.ts +++ b/shared/src/i18n/ca/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/ca/mailAdmin.ts b/shared/src/i18n/ca/mailAdmin.ts new file mode 100644 index 000000000..4375a1092 --- /dev/null +++ b/shared/src/i18n/ca/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Importació de correus de reserva', + 'mailAdmin.hint': 'Les confirmacions de reserva reenviades es converteixen en reserves del viatge correcte.', + 'mailAdmin.enabled': 'Importa correus de reserva', + 'mailAdmin.enabledHint': + 'Dona a tothom en aquesta instal·lació una adreça de reenviament. Desactivat, cap adreça rep correu.', + 'mailAdmin.transport.label': 'Com arriba el correu', + 'mailAdmin.transport.off': 'Només pujada de fitxers', + 'mailAdmin.transport.imap': 'Bústia IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Correus amb IA per persona i dia', + 'mailAdmin.aiDailyHint': 'Per sobre d’aquest nombre, només es llegeixen dades de reserva llegibles per màquina.', + 'mailAdmin.template': 'Plantilla d’adreça', + 'mailAdmin.templateHint': 'L’adreça de reenviament, amb {token} allà on va el codi de cada persona.', + 'mailAdmin.imap.host': 'Servidor', + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Xifratge', + 'mailAdmin.imap.user': 'Usuari', + 'mailAdmin.imap.pass': 'Contrasenya', + 'mailAdmin.imap.mailbox': 'Carpeta', + 'mailAdmin.imap.mailboxHint': 'Els correus llegits es mouen a TREK/processed.', + 'mailAdmin.webhook.secret': 'Secret de signatura', + 'mailAdmin.webhook.secretHint': + 'Almenys 32 caràcters; el remitent signa cada correu amb ell. Buit, el webhook es desactiva.', + 'mailAdmin.inboundUrl': 'Adreça del webhook', + 'mailAdmin.inboundUrlHint': 'El teu servei de correu hi envia cada correu, en brut i signat.', + 'mailAdmin.copyUrl': 'Copia l’adreça', + 'mailAdmin.envHint': 'Les variables d’entorn (MAIL_INGEST_*) tenen prioritat sobre aquests camps.', + 'mailAdmin.loadError': 'No s’ha pogut carregar la configuració d’importació de correu.', + 'mailAdmin.problem.secretShort': 'El secret necessita almenys 32 caràcters.', + 'mailAdmin.problem.portInvalid': 'Introdueix un port entre 1 i 65535.', + 'mailAdmin.problem.templateToken': 'L’adreça necessita {token} per al codi de cada persona.', + 'mailAdmin.problem.dailyInvalid': 'Introdueix un nombre enter de 0 a 1000.', + 'mailAdmin.status.title': 'Estat', + 'mailAdmin.status.lastRead': 'Darrera lectura {time}', + 'mailAdmin.status.never': 'mai', + 'mailAdmin.status.mails': 'Correus, 24 h', + 'mailAdmin.status.added': 'Afegits automàticament', + 'mailAdmin.status.waiting': 'En espera', + 'mailAdmin.status.failed': 'Fallits', + 'mailAdmin.test.run': 'Prova la connexió', + 'mailAdmin.test.ok': 'Connectat', + 'mailAdmin.test.failed': 'Sense connexió', +}; +export default mailAdmin; diff --git a/shared/src/i18n/ca/mailInbox.ts b/shared/src/i18n/ca/mailInbox.ts new file mode 100644 index 000000000..f1d75b76a --- /dev/null +++ b/shared/src/i18n/ca/mailInbox.ts @@ -0,0 +1,99 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Reserves pendents', + 'mailInbox.pendingCount': '{count} reserves t’esperen', + 'mailInbox.pendingCount.one': '{count} reserva t’espera', + 'mailInbox.empty': 'Res pendent', + 'mailInbox.emptyHint': + 'Les reserves que TREK no ha pogut afegir per ell mateix apareixen aquí, amb el que pots fer en cada cas.', + 'mailInbox.untitled': 'Correu de reserva', + 'mailInbox.showMail': 'Mostra el correu', + 'mailInbox.backToList': 'Torna a la llista', + 'mailInbox.loadMore': 'Mostra’n més', + 'mailInbox.pickTrip': 'Quin viatge?', + 'mailInbox.added': 'Reserva afegida', + 'mailInbox.filed': 'Desat als fitxers del viatge', + 'mailInbox.discarded': 'Descartat', + 'mailInbox.actionError': 'No ha funcionat. Torna-ho a provar d’aquí a un moment.', + 'mailInbox.previewError': 'No s’ha pogut mostrar el correu.', + 'mailInbox.action.add': 'Afegeix', + 'mailInbox.action.choose': 'Afegeix a un viatge', + 'mailInbox.action.file': 'Desa com a document', + 'mailInbox.action.manual': 'Omple-ho tu mateix', + 'mailInbox.action.discard': 'Descarta', + 'mailInbox.reason.forwardingConfirmation': + 'El teu proveïdor de correu demana confirmar el reenviament. Introdueix-hi aquest codi.', + 'mailInbox.reason.newsletter': 'Un butlletí, no una reserva', + 'mailInbox.reason.autoReply': 'Una resposta automàtica', + 'mailInbox.reason.emptyMail': 'El correu no té text ni adjunt per llegir', + 'mailInbox.reason.mailUnreadable': 'No s’ha pogut llegir el correu', + 'mailInbox.reason.notABooking': 'No s’ha trobat cap reserva en aquest correu', + 'mailInbox.reason.tooLarge': 'El correu és massa gran per llegir-lo', + 'mailInbox.reason.addressInvalid': 'Va arribar a una adreça que ja no funciona', + 'mailInbox.reason.addressDailyLimit': 'La teva adreça ja ha rebut tots els correus d’avui', + 'mailInbox.reason.processingFailed': 'La lectura ha fallat diverses vegades', + 'mailInbox.reason.aiQuotaExhausted': 'S’ha esgotat la quota d’IA d’aquest període', + 'mailInbox.reason.aiDisabled': 'Llegit sense IA, que no ha trobat cap reserva', + 'mailInbox.reason.aiUserCap': 'Has arribat al teu límit diari de correus llegits amb IA', + 'mailInbox.reason.aiProviderError': 'La IA no ho ha pogut llegir ara', + 'mailInbox.reason.noTripInRange': 'Cap viatge cobreix aquestes dates', + 'mailInbox.reason.tripAmbiguous': 'Podria tractar-se de més d’un viatge', + 'mailInbox.reason.tripWithoutDates': 'El viatge no té dates amb què comparar', + 'mailInbox.reason.noEditPermission': 'No pots afegir reserves a aquest viatge', + 'mailInbox.reason.demoMode': 'La demo no afegeix reserves', + 'mailInbox.reason.outsideTripDates': 'Una data queda fora del viatge', + 'mailInbox.reason.sourceMismatch': 'Les dades del correu no coincideixen entre elles', + 'mailInbox.reason.implausible': 'Un valor no pot ser correcte', + 'mailInbox.reason.notInText': 'Un valor llegit no apareix al correu', + 'mailInbox.reason.imageOnly': 'Llegit només a partir d’una imatge, per tant necessita la teva revisió', + 'mailInbox.reason.missingCriticalField': 'Falta el tipus, la data o el trajecte', + 'mailInbox.reason.missingConfirmation': 'No s’ha trobat cap localitzador de reserva', + 'mailInbox.reason.tooManyBookings': 'Hi ha més reserves en un sol correu de les que s’afegeixen automàticament', + 'mailInbox.reason.changeOrCancellation': 'Un canvi o una cancel·lació d’una reserva anterior', + 'mailInbox.reason.cancelled': 'La reserva està cancel·lada', + 'mailInbox.reason.possibleDuplicate': 'Potser el viatge ja té aquesta reserva', + 'mailInbox.reason.changeAvailable': 'El viatge ja té aquesta reserva, i el correu la modifica', + 'mailInbox.reason.conflict': 'El viatge té aquesta reserva amb altres dades', + 'mailInbox.reason.askFirst': 'Has demanat confirmar cada reserva', + 'mailInbox.settings.title': 'Correus de reserva', + 'mailInbox.settings.on': 'Activat', + 'mailInbox.settings.off': 'Desactivat', + 'mailInbox.settings.yourAddress': 'La teva adreça', + 'mailInbox.settings.copy': 'Copia l’adreça', + 'mailInbox.settings.copyError': 'No s’ha pogut copiar', + 'mailInbox.settings.renew': 'Nova adreça', + 'mailInbox.settings.renewConfirm': 'Toca de nou: l’adreça anterior deixarà de funcionar', + 'mailInbox.settings.renewed': 'La teva nova adreça està a punt', + 'mailInbox.settings.saveError': 'No s’ha pogut desar', + 'mailInbox.settings.addressHint': + 'Reenvia les confirmacions de reserva a aquesta adreça i TREK les afegeix al viatge correcte. Mantén-la en privat, com una contrasenya.', + 'mailInbox.settings.howTo': 'Reenvia automàticament', + 'mailInbox.settings.howToGmail': + 'Gmail: Configuració, Reenviament i correu POP/IMAP, Afegeix una adreça de reenviament. Gmail envia un codi, que apareix a Reserves pendents. Després afegeix un filtre per als remitents de reserves que reenviï a TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Configuració, Correu, Regles, afegeix una regla que reenviï els correus de reserva. Alguns comptes de feina bloquegen el reenviament automàtic; allà reenvia a mà.', + 'mailInbox.settings.howToManual': 'A mà: reenvia qualsevol correu de reserva, amb els adjunts.', + 'mailInbox.settings.enabled': 'Importa correus de reserva', + 'mailInbox.settings.enabledHint': 'Desactivat, els correus enviats a la teva adreça no es llegeixen.', + 'mailInbox.settings.ai': 'Llegeix amb IA', + 'mailInbox.settings.aiHint': + 'Llegeix qualsevol correu de reserva i en troba el viatge. El correu i les dates i destinacions dels teus viatges s’envien al proveïdor d’IA.', + 'mailInbox.settings.aiUnavailable': + 'No disponible aquí: només es llegeixen correus amb dades de reserva llegibles per màquina.', + 'mailInbox.settings.autoAdd': 'Afegeix automàticament', + 'mailInbox.settings.autoAddHint': + 'Una reserva de la qual TREK n’està segur va directa al seu viatge, i pots desfer-ho durant 7 dies. Desactivat, cada una espera la teva aprovació.', + 'mailInbox.settings.upload': 'Puja un fitxer de correu', + 'mailInbox.settings.uploaded': 'Correu rebut, s’està llegint', + 'mailInbox.settings.uploadDuplicate': 'Aquest correu ja està importat', + 'mailInbox.settings.uploadError': 'No s’ha pogut llegir el fitxer com a correu', + 'mailInbox.settings.privacy': + 'Els correus reenviats es conserven xifrats un màxim de 30 dies i després s’eliminen. Si us plau, no reenviïs documents d’identitat ni de salut.', + 'mailInbox.trip.title': 'O reenvia per correu', + 'mailInbox.trip.hint': 'Un correu de reserva enviat a l’adreça d’aquest viatge arriba aquí sense endevinar res.', + 'mailInbox.trip.create': 'Crea una adreça per a aquest viatge', + 'mailInbox.fromMail': 'Des de correu', + 'mailInbox.fromMailHint': 'Afegit des d’un correu de reserva reenviat', +}; +export default mailInbox; diff --git a/shared/src/i18n/ca/notif.ts b/shared/src/i18n/ca/notif.ts index f22c10529..3b31c28f6 100644 --- a/shared/src/i18n/ca/notif.ts +++ b/shared/src/i18n/ca/notif.ts @@ -64,7 +64,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} s’ha afegit a {trip}', 'notif.mail_booking_added.shared': '{actor} ha afegit {booking} a {trip} des d’un correu reenviat', 'notif.mail_booking_pending.title': 'Reserves reenviades per revisar', - 'notif.mail_booking_pending.text': 'Reserves reenviades pendents: {count}', + 'notif.mail_booking_pending.text': '{count} reserves reenviades t’esperen', + 'notif.mail_booking_pending.text.one': '{count} reserva reenviada t’espera', 'notif.mail_ingest_paused.title': 'Importació de correus en pausa', 'notif.mail_ingest_paused.text': 'De moment no s’accepten els correus de reserva reenviats. Les teves adreces es mantenen i tornaran a funcionar quan torni la importació.', diff --git a/shared/src/i18n/ca/reservations.ts b/shared/src/i18n/ca/reservations.ts index cf96f1d86..5babab2d3 100644 --- a/shared/src/i18n/ca/reservations.ts +++ b/shared/src/i18n/ca/reservations.ts @@ -119,7 +119,10 @@ const reservations: TranslationStrings = { 'reservations.layover.layover': 'Escala', 'reservations.needsReview': 'Revisa', 'reservations.needsReviewHint': - "No s'ha pogut identificar l'aeroport automàticament — si us plau, confirma la ubicació.", + 'Algunes dades s’han emplenat automàticament i no s’han pogut llegir amb seguretat. Si us plau, revisa-les.', + 'reservations.reviewConfirm': 'És correcte', + 'reservations.reviewConfirmHint': 'Fes clic per marcar-ho com a revisat.', + 'reservations.reviewConfirmed': 'Marcat com a revisat', 'reservations.searchLocation': 'Cerca estació, port, adreça...', 'reservations.meta.trainNumber': 'Núm. de tren', 'reservations.meta.platform': 'Andana', diff --git a/shared/src/i18n/ca/settings.ts b/shared/src/i18n/ca/settings.ts index 9d8e7ebfc..e705fbd01 100644 --- a/shared/src/i18n/ca/settings.ts +++ b/shared/src/i18n/ca/settings.ts @@ -98,6 +98,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Retards de vols', 'settings.notifyFlightLanded': 'Aterratges', 'settings.notifyTripReminder': 'Recordatoris de viatge', + 'settings.notifyMailBookingAdded': 'Reserves afegides des de correus', + 'settings.notifyMailBookingPending': 'Reserves reenviades per revisar', + 'settings.notifyMailIngestPaused': 'Importació de correu en pausa', 'settings.notifyTodoDue': 'Tasca propera', 'settings.notifyVacayInvite': 'Invitacions de fusió Vacay', 'settings.notifyVacayShare': 'Comparticions de calendari Vacay', diff --git a/shared/src/i18n/cs/index.ts b/shared/src/i18n/cs/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/cs/index.ts +++ b/shared/src/i18n/cs/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/cs/mailAdmin.ts b/shared/src/i18n/cs/mailAdmin.ts new file mode 100644 index 000000000..2d4ce9740 --- /dev/null +++ b/shared/src/i18n/cs/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Import e-mailů s rezervacemi', + 'mailAdmin.hint': 'Přeposlaná potvrzení rezervací se stanou rezervacemi ve správném výletu.', + 'mailAdmin.enabled': 'Importovat e-maily s rezervacemi', + 'mailAdmin.enabledHint': + 'Dá každému na této instalaci adresu pro přeposílání. Při vypnutí žádná adresa nepřijímá poštu.', + 'mailAdmin.transport.label': 'Jak e-maily přicházejí', + 'mailAdmin.transport.off': 'Pouze nahrávání', + 'mailAdmin.transport.imap': 'Schránka IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'E-maily s AI na osobu a den', + 'mailAdmin.aiDailyHint': 'Nad tento limit se čtou jen strojově čitelné údaje o rezervaci.', + 'mailAdmin.template': 'Šablona adresy', + 'mailAdmin.templateHint': 'Adresa pro přeposílání, kde {token} nahradí kód každého uživatele.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Šifrování', + 'mailAdmin.imap.user': 'Uživatel', + 'mailAdmin.imap.pass': 'Heslo', + 'mailAdmin.imap.mailbox': 'Složka', + 'mailAdmin.imap.mailboxHint': 'Přečtené e-maily se přesunou do TREK/processed.', + 'mailAdmin.webhook.secret': 'Podpisový klíč', + 'mailAdmin.webhook.secretHint': + 'Alespoň 32 znaků; odesílatel jím podepisuje každý e-mail. Prázdný klíč webhook vypne.', + 'mailAdmin.inboundUrl': 'Adresa webhooku', + 'mailAdmin.inboundUrlHint': 'Tvoje e-mailová služba sem posílá každý e-mail, nezpracovaný a podepsaný.', + 'mailAdmin.copyUrl': 'Kopírovat adresu', + 'mailAdmin.envHint': 'Proměnné prostředí (MAIL_INGEST_*) mají přednost před těmito poli.', + 'mailAdmin.loadError': 'Nastavení importu e-mailů se nepodařilo načíst.', + 'mailAdmin.problem.secretShort': 'Klíč musí mít alespoň 32 znaků.', + 'mailAdmin.problem.portInvalid': 'Zadej port mezi 1 a 65535.', + 'mailAdmin.problem.templateToken': 'Adresa musí obsahovat {token} pro kód každého uživatele.', + 'mailAdmin.problem.dailyInvalid': 'Zadej celé číslo od 0 do 1000.', + 'mailAdmin.status.title': 'Stav', + 'mailAdmin.status.lastRead': 'Naposledy přečteno {time}', + 'mailAdmin.status.never': 'nikdy', + 'mailAdmin.status.mails': 'E-maily, 24 h', + 'mailAdmin.status.added': 'Přidáno automaticky', + 'mailAdmin.status.waiting': 'Čeká', + 'mailAdmin.status.failed': 'Selhalo', + 'mailAdmin.test.run': 'Otestovat připojení', + 'mailAdmin.test.ok': 'Připojeno', + 'mailAdmin.test.failed': 'Bez připojení', +}; +export default mailAdmin; diff --git a/shared/src/i18n/cs/mailInbox.ts b/shared/src/i18n/cs/mailInbox.ts new file mode 100644 index 000000000..98a32db69 --- /dev/null +++ b/shared/src/i18n/cs/mailInbox.ts @@ -0,0 +1,98 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Rezervace čekající na schválení', + 'mailInbox.pendingCount': '{count} rezervací čeká na tebe', + 'mailInbox.pendingCount.one': '{count} rezervace čeká na tebe', + 'mailInbox.pendingCount.few': '{count} rezervace čekají na tebe', + 'mailInbox.empty': 'Nic nečeká', + 'mailInbox.emptyHint': 'Rezervace, které TREK nedokázal přidat sám, se zobrazí tady i s možnostmi, co s nimi udělat.', + 'mailInbox.untitled': 'E-mail s rezervací', + 'mailInbox.showMail': 'Zobrazit e-mail', + 'mailInbox.backToList': 'Zpět na seznam', + 'mailInbox.loadMore': 'Zobrazit více', + 'mailInbox.pickTrip': 'Který výlet?', + 'mailInbox.added': 'Rezervace přidána', + 'mailInbox.filed': 'Uloženo do souborů výletu', + 'mailInbox.discarded': 'Zahozeno', + 'mailInbox.actionError': 'To se nepovedlo. Zkus to za chvíli znovu.', + 'mailInbox.previewError': 'E-mail se nepodařilo zobrazit.', + 'mailInbox.action.add': 'Přidat', + 'mailInbox.action.choose': 'Přidat k výletu', + 'mailInbox.action.file': 'Uložit jako dokument', + 'mailInbox.action.manual': 'Vyplnit ručně', + 'mailInbox.action.discard': 'Zahodit', + 'mailInbox.reason.forwardingConfirmation': + 'Tvůj poskytovatel e-mailu žádá o potvrzení přeposílání. Zadej tam tento kód.', + 'mailInbox.reason.newsletter': 'Newsletter, ne rezervace', + 'mailInbox.reason.autoReply': 'Automatická odpověď', + 'mailInbox.reason.emptyMail': 'E-mail neobsahuje žádný text ani přílohu ke čtení', + 'mailInbox.reason.mailUnreadable': 'E-mail se nepodařilo přečíst', + 'mailInbox.reason.notABooking': 'V tomto e-mailu nebyla nalezena žádná rezervace', + 'mailInbox.reason.tooLarge': 'E-mail je příliš velký ke čtení', + 'mailInbox.reason.addressInvalid': 'Přišel na adresu, která už nefunguje', + 'mailInbox.reason.addressDailyLimit': 'Tvoje adresa dnes přijala maximum e-mailů', + 'mailInbox.reason.processingFailed': 'Čtení se několikrát nezdařilo', + 'mailInbox.reason.aiQuotaExhausted': 'Limit AI pro toto období je vyčerpán', + 'mailInbox.reason.aiDisabled': 'Přečteno bez AI, která nenašla žádnou rezervaci', + 'mailInbox.reason.aiUserCap': 'Dosáhl jsi denního limitu e-mailů čtených pomocí AI', + 'mailInbox.reason.aiProviderError': 'AI to teď nedokázala přečíst', + 'mailInbox.reason.noTripInRange': 'Žádný výlet nepokrývá tato data', + 'mailInbox.reason.tripAmbiguous': 'Může jít o více než jeden výlet', + 'mailInbox.reason.tripWithoutDates': 'Výlet nemá data, se kterými by se dalo porovnat', + 'mailInbox.reason.noEditPermission': 'Do tohoto výletu nesmíš přidávat rezervace', + 'mailInbox.reason.demoMode': 'Demo nepřidává žádné rezervace', + 'mailInbox.reason.outsideTripDates': 'Datum leží mimo výlet', + 'mailInbox.reason.sourceMismatch': 'Údaje v e-mailu si navzájem odporují', + 'mailInbox.reason.implausible': 'Některá hodnota nemůže být správná', + 'mailInbox.reason.notInText': 'Načtená hodnota v e-mailu není', + 'mailInbox.reason.imageOnly': 'Přečteno jen z obrázku, proto je potřeba, aby ses na to podíval', + 'mailInbox.reason.missingCriticalField': 'Chybí typ, datum nebo trasa', + 'mailInbox.reason.missingConfirmation': 'Nebylo nalezeno číslo rezervace', + 'mailInbox.reason.tooManyBookings': 'V jednom e-mailu je více rezervací, než se přidává automaticky', + 'mailInbox.reason.changeOrCancellation': 'Změna nebo zrušení dřívější rezervace', + 'mailInbox.reason.cancelled': 'Rezervace je zrušena', + 'mailInbox.reason.possibleDuplicate': 'Výlet už tuto rezervaci možná obsahuje', + 'mailInbox.reason.changeAvailable': 'Výlet tuto rezervaci obsahuje a e-mail ji mění', + 'mailInbox.reason.conflict': 'Výlet obsahuje tuto rezervaci s jinými údaji', + 'mailInbox.reason.askFirst': 'Chtěl jsi potvrzovat každou rezervaci', + 'mailInbox.settings.title': 'E-maily s rezervacemi', + 'mailInbox.settings.on': 'Zapnuto', + 'mailInbox.settings.off': 'Vypnuto', + 'mailInbox.settings.yourAddress': 'Tvoje adresa', + 'mailInbox.settings.copy': 'Kopírovat adresu', + 'mailInbox.settings.copyError': 'Nepodařilo se zkopírovat', + 'mailInbox.settings.renew': 'Nová adresa', + 'mailInbox.settings.renewConfirm': 'Klepni znovu: stará adresa přestane fungovat', + 'mailInbox.settings.renewed': 'Tvoje nová adresa je připravena', + 'mailInbox.settings.saveError': 'Nepodařilo se uložit', + 'mailInbox.settings.addressHint': + 'Přeposílej potvrzení rezervací na tuto adresu a TREK je přidá do správného výletu. Chraň ji jako heslo.', + 'mailInbox.settings.howTo': 'Přeposílat automaticky', + 'mailInbox.settings.howToGmail': + 'Gmail: Nastavení, Přeposílání a POP/IMAP, Přidat adresu pro přeposílání. Gmail pošle kód, který se zobrazí v části Rezervace čekající na schválení. Poté přidej filtr pro odesílatele rezervací, který přeposílá na TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Nastavení, Pošta, Pravidla, přidej pravidlo, které přeposílá e-maily s rezervacemi. Některé pracovní účty automatické přeposílání blokují, tam přeposílej ručně.', + 'mailInbox.settings.howToManual': 'Ručně: přepošli jakýkoli e-mail s rezervací, včetně příloh.', + 'mailInbox.settings.enabled': 'Importovat e-maily s rezervacemi', + 'mailInbox.settings.enabledHint': 'Při vypnutí se e-maily na tvou adresu nečtou.', + 'mailInbox.settings.ai': 'Číst pomocí AI', + 'mailInbox.settings.aiHint': + 'Přečte jakýkoli e-mail s rezervací a najde k němu výlet. E-mail a data a cíle tvých výletů se odesílají poskytovateli AI.', + 'mailInbox.settings.aiUnavailable': 'Zde není dostupné: čtou se jen e-maily se strojově čitelnými údaji o rezervaci.', + 'mailInbox.settings.autoAdd': 'Přidávat automaticky', + 'mailInbox.settings.autoAddHint': + 'Rezervace, ve které si je TREK jistý, se rovnou přidá do výletu a 7 dní ji můžeš vrátit zpět. Při vypnutí každá čeká na tvé potvrzení.', + 'mailInbox.settings.upload': 'Nahrát soubor e-mailu', + 'mailInbox.settings.uploaded': 'E-mail přijat, právě se čte', + 'mailInbox.settings.uploadDuplicate': 'Tento e-mail už je zde', + 'mailInbox.settings.uploadError': 'Soubor se nepodařilo přečíst jako e-mail', + 'mailInbox.settings.privacy': + 'Přeposlané e-maily se uchovávají zašifrované nejdéle 30 dní a poté se smažou. Nepřeposílej prosím doklady totožnosti ani zdravotní dokumenty.', + 'mailInbox.trip.title': 'Nebo přepošli e-mailem', + 'mailInbox.trip.hint': 'E-mail s rezervací poslaný na adresu tohoto výletu skončí tady, bez hádání.', + 'mailInbox.trip.create': 'Vytvořit adresu pro tento výlet', + 'mailInbox.fromMail': 'Z e-mailu', + 'mailInbox.fromMailHint': 'Přidáno z přeposlaného e-mailu s rezervací', +}; +export default mailInbox; diff --git a/shared/src/i18n/cs/notif.ts b/shared/src/i18n/cs/notif.ts index 39768ad1b..f79f8bd22 100644 --- a/shared/src/i18n/cs/notif.ts +++ b/shared/src/i18n/cs/notif.ts @@ -60,7 +60,9 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} byla přidána do {trip}', 'notif.mail_booking_added.shared': '{actor} přidal(a) {booking} do {trip} z přeposlaného e-mailu', 'notif.mail_booking_pending.title': 'Přeposlané rezervace ke kontrole', - 'notif.mail_booking_pending.text': 'Přeposlané rezervace čekající na vás: {count}', + 'notif.mail_booking_pending.text': '{count} přeposlaných rezervací čeká na vás', + 'notif.mail_booking_pending.text.one': '{count} přeposlaná rezervace čeká na vás', + 'notif.mail_booking_pending.text.few': '{count} přeposlané rezervace čekají na vás', 'notif.mail_ingest_paused.title': 'Import e-mailů pozastaven', 'notif.mail_ingest_paused.text': 'Přeposlané e-maily s rezervacemi se zatím nepřijímají. Vaše adresy zůstávají a znovu budou fungovat, až se import vrátí.', diff --git a/shared/src/i18n/cs/reservations.ts b/shared/src/i18n/cs/reservations.ts index 738aac192..6f376e132 100644 --- a/shared/src/i18n/cs/reservations.ts +++ b/shared/src/i18n/cs/reservations.ts @@ -35,7 +35,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Přípoj', 'reservations.layover.layover': 'Mezipřistání', 'reservations.needsReview': 'Zkontrolovat', - 'reservations.needsReviewHint': 'Letiště nebylo možné automaticky rozpoznat — potvrďte prosím místo.', + 'reservations.needsReviewHint': + 'Některé údaje se vyplnily automaticky a nešlo je přečíst s jistotou. Zkontroluj je prosím.', + 'reservations.reviewConfirm': 'Souhlasí', + 'reservations.reviewConfirmHint': 'Kliknutím označíš jako zkontrolované.', + 'reservations.reviewConfirmed': 'Označeno jako zkontrolované', 'reservations.searchLocation': 'Hledat stanici, přístav, adresu...', 'reservations.meta.trainNumber': 'Číslo vlaku', 'reservations.meta.platform': 'Nástupiště', diff --git a/shared/src/i18n/cs/settings.ts b/shared/src/i18n/cs/settings.ts index 5a6e99ae3..1f5605c3d 100644 --- a/shared/src/i18n/cs/settings.ts +++ b/shared/src/i18n/cs/settings.ts @@ -112,6 +112,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Zpoždění letů', 'settings.notifyFlightLanded': 'Přistání', 'settings.notifyTripReminder': 'Připomínky cest', + 'settings.notifyMailBookingAdded': 'Rezervace přidané z e-mailů', + 'settings.notifyMailBookingPending': 'Přeposlané rezervace ke kontrole', + 'settings.notifyMailIngestPaused': 'Import e-mailů pozastaven', 'settings.notifyTodoDue': 'Úkol se blíží', 'settings.notifyVacayInvite': 'Pozvánky k propojení Vacay', 'settings.notifyVacayShare': 'Sdílení kalendáře Vacay', diff --git a/shared/src/i18n/de/index.ts b/shared/src/i18n/de/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/de/index.ts +++ b/shared/src/i18n/de/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/de/mailAdmin.ts b/shared/src/i18n/de/mailAdmin.ts new file mode 100644 index 000000000..7921525b3 --- /dev/null +++ b/shared/src/i18n/de/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Buchungsmail-Import', + 'mailAdmin.hint': 'Weitergeleitete Buchungsbestätigungen werden zu Buchungen in der passenden Reise.', + 'mailAdmin.enabled': 'Buchungsmails importieren', + 'mailAdmin.enabledHint': + 'Gibt allen auf dieser Instanz eine Weiterleitungsadresse. Aus, nimmt keine Adresse Mails an.', + 'mailAdmin.transport.label': 'Wie Mails ankommen', + 'mailAdmin.transport.off': 'Nur Hochladen', + 'mailAdmin.transport.imap': 'IMAP-Postfach', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'AI-Mails pro Person und Tag', + 'mailAdmin.aiDailyHint': 'Darüber werden nur maschinenlesbare Buchungsdaten gelesen.', + 'mailAdmin.template': 'Adressvorlage', + 'mailAdmin.templateHint': 'Die Weiterleitungsadresse, mit {token} an der Stelle des persönlichen Codes.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Verschlüsselung', + 'mailAdmin.imap.user': 'Benutzer', + 'mailAdmin.imap.pass': 'Passwort', + 'mailAdmin.imap.mailbox': 'Ordner', + 'mailAdmin.imap.mailboxHint': 'Gelesene Mails wandern weiter nach TREK/processed.', + 'mailAdmin.webhook.secret': 'Signaturschlüssel', + 'mailAdmin.webhook.secretHint': + 'Mindestens 32 Zeichen, der Absender signiert damit jede Mail. Leer schaltet den Webhook aus.', + 'mailAdmin.inboundUrl': 'Webhook-Adresse', + 'mailAdmin.inboundUrlHint': 'Dein Mail-Dienst schickt jede Mail roh und signiert an diese Adresse.', + 'mailAdmin.copyUrl': 'Adresse kopieren', + 'mailAdmin.envHint': 'Umgebungsvariablen (MAIL_INGEST_*) haben Vorrang vor diesen Feldern.', + 'mailAdmin.loadError': 'Die Einstellungen des Mail-Imports konnten nicht geladen werden.', + 'mailAdmin.problem.secretShort': 'Der Schlüssel braucht mindestens 32 Zeichen.', + 'mailAdmin.problem.portInvalid': 'Gib einen Port zwischen 1 und 65535 ein.', + 'mailAdmin.problem.templateToken': 'Die Adresse braucht {token} für den persönlichen Code.', + 'mailAdmin.problem.dailyInvalid': 'Gib eine ganze Zahl von 0 bis 1000 ein.', + 'mailAdmin.status.title': 'Status', // same-as-en + 'mailAdmin.status.lastRead': 'Zuletzt gelesen {time}', + 'mailAdmin.status.never': 'noch nie', + 'mailAdmin.status.mails': 'Mails, 24 Std.', + 'mailAdmin.status.added': 'Selbst eingetragen', + 'mailAdmin.status.waiting': 'Offen', + 'mailAdmin.status.failed': 'Fehlgeschlagen', + 'mailAdmin.test.run': 'Verbindung testen', + 'mailAdmin.test.ok': 'Verbunden', + 'mailAdmin.test.failed': 'Keine Verbindung', +}; +export default mailAdmin; diff --git a/shared/src/i18n/de/mailInbox.ts b/shared/src/i18n/de/mailInbox.ts new file mode 100644 index 000000000..e4c5be438 --- /dev/null +++ b/shared/src/i18n/de/mailInbox.ts @@ -0,0 +1,99 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Offene Buchungen', + 'mailInbox.pendingCount': '{count} Buchungen warten auf dich', + 'mailInbox.pendingCount.one': '{count} Buchung wartet auf dich', + 'mailInbox.empty': 'Nichts offen', + 'mailInbox.emptyHint': + 'Hier erscheinen Buchungen, die TREK nicht selbst eintragen konnte, mit dem, was du jeweils tun kannst.', + 'mailInbox.untitled': 'Buchungsmail', + 'mailInbox.showMail': 'Mail anzeigen', + 'mailInbox.backToList': 'Zurück zur Liste', + 'mailInbox.loadMore': 'Mehr anzeigen', + 'mailInbox.pickTrip': 'Welche Reise?', + 'mailInbox.added': 'Buchung eingetragen', + 'mailInbox.filed': 'In den Dateien der Reise abgelegt', + 'mailInbox.discarded': 'Verworfen', + 'mailInbox.actionError': 'Das hat nicht geklappt. Versuch es gleich noch einmal.', + 'mailInbox.previewError': 'Die Mail konnte nicht angezeigt werden.', + 'mailInbox.action.add': 'Eintragen', + 'mailInbox.action.choose': 'Zu einer Reise', + 'mailInbox.action.file': 'Als Dokument ablegen', + 'mailInbox.action.manual': 'Selbst ausfüllen', + 'mailInbox.action.discard': 'Verwerfen', + 'mailInbox.reason.forwardingConfirmation': + 'Dein Mailanbieter möchte die Weiterleitung bestätigt haben. Gib dort diesen Code ein.', + 'mailInbox.reason.newsletter': 'Ein Newsletter, keine Buchung', + 'mailInbox.reason.autoReply': 'Eine automatische Antwort', + 'mailInbox.reason.emptyMail': 'Die Mail hat keinen Text und keinen Anhang zum Lesen', + 'mailInbox.reason.mailUnreadable': 'Die Mail konnte nicht gelesen werden', + 'mailInbox.reason.notABooking': 'In dieser Mail wurde keine Buchung gefunden', + 'mailInbox.reason.tooLarge': 'Die Mail ist zu groß zum Lesen', + 'mailInbox.reason.addressInvalid': 'Sie kam an eine Adresse, die nicht mehr gilt', + 'mailInbox.reason.addressDailyLimit': 'Deine Adresse hat für heute alle Mails angenommen', + 'mailInbox.reason.processingFailed': 'Das Lesen ist mehrmals fehlgeschlagen', + 'mailInbox.reason.aiQuotaExhausted': 'Das AI-Kontingent dieses Zeitraums ist aufgebraucht', + 'mailInbox.reason.aiDisabled': 'Ohne AI gelesen, dabei wurde keine Buchung gefunden', + 'mailInbox.reason.aiUserCap': 'Dein Tageslimit für Mails mit AI ist erreicht', + 'mailInbox.reason.aiProviderError': 'Die AI konnte sie gerade nicht lesen', + 'mailInbox.reason.noTripInRange': 'Keine Reise passt zu diesen Daten', + 'mailInbox.reason.tripAmbiguous': 'Mehrere Reisen kommen infrage', + 'mailInbox.reason.tripWithoutDates': 'Die Reise hat keine Daten zum Abgleichen', + 'mailInbox.reason.noEditPermission': 'Du darfst in dieser Reise keine Buchungen eintragen', + 'mailInbox.reason.demoMode': 'Die Demo trägt keine Buchungen ein', + 'mailInbox.reason.outsideTripDates': 'Ein Datum liegt außerhalb der Reise', + 'mailInbox.reason.sourceMismatch': 'Die Angaben der Mail widersprechen sich', + 'mailInbox.reason.implausible': 'Ein Wert kann nicht stimmen', + 'mailInbox.reason.notInText': 'Ein gelesener Wert steht nicht in der Mail', + 'mailInbox.reason.imageOnly': 'Nur aus einem Bild gelesen, deshalb bitte prüfen', + 'mailInbox.reason.missingCriticalField': 'Art, Datum oder Strecke fehlen', + 'mailInbox.reason.missingConfirmation': 'Keine Buchungsnummer gefunden', + 'mailInbox.reason.tooManyBookings': 'Mehr Buchungen in einer Mail, als von selbst eingetragen werden', + 'mailInbox.reason.changeOrCancellation': 'Eine Änderung oder Stornierung einer früheren Buchung', + 'mailInbox.reason.cancelled': 'Die Buchung ist storniert', + 'mailInbox.reason.possibleDuplicate': 'Die Reise enthält diese Buchung vielleicht schon', + 'mailInbox.reason.changeAvailable': 'Die Reise enthält diese Buchung, und die Mail ändert sie', + 'mailInbox.reason.conflict': 'Die Reise enthält diese Buchung mit anderen Angaben', + 'mailInbox.reason.askFirst': 'Du möchtest jede Buchung erst bestätigen', + 'mailInbox.settings.title': 'Buchungen per Mail', + 'mailInbox.settings.on': 'An', + 'mailInbox.settings.off': 'Aus', + 'mailInbox.settings.yourAddress': 'Deine Adresse', + 'mailInbox.settings.copy': 'Adresse kopieren', + 'mailInbox.settings.copyError': 'Kopieren nicht möglich', + 'mailInbox.settings.renew': 'Neue Adresse', + 'mailInbox.settings.renewConfirm': 'Nochmal tippen: Die alte Adresse gilt dann nicht mehr', + 'mailInbox.settings.renewed': 'Deine neue Adresse ist bereit', + 'mailInbox.settings.saveError': 'Speichern nicht möglich', + 'mailInbox.settings.addressHint': + 'Leite Buchungsbestätigungen an diese Adresse weiter, TREK trägt sie in die passende Reise ein. Behandle sie wie ein Passwort.', + 'mailInbox.settings.howTo': 'Automatisch weiterleiten', + 'mailInbox.settings.howToGmail': + 'Gmail: Einstellungen, Weiterleitung und POP/IMAP, Weiterleitungsadresse hinzufügen. Gmail schickt einen Code, der unter Offene Buchungen erscheint. Danach einen Filter für Buchungsabsender anlegen, der an TREK weiterleitet.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Einstellungen, E-Mail, Regeln, eine Regel zum Weiterleiten von Buchungsmails anlegen. Manche Firmenkonten sperren das automatische Weiterleiten; dort von Hand weiterleiten.', + 'mailInbox.settings.howToManual': 'Von Hand: Jede Buchungsmail mit Anhängen einfach weiterleiten.', + 'mailInbox.settings.enabled': 'Buchungsmails importieren', + 'mailInbox.settings.enabledHint': 'Aus, werden Mails an deine Adresse nicht gelesen.', + 'mailInbox.settings.ai': 'Mit AI lesen', + 'mailInbox.settings.aiHint': + 'Liest jede Buchungsmail und findet die Reise. Die Mail sowie Daten und Ziele deiner Reisen gehen an den AI-Anbieter.', + 'mailInbox.settings.aiUnavailable': + 'Hier nicht verfügbar: Gelesen werden nur Mails mit maschinenlesbaren Buchungsdaten.', + 'mailInbox.settings.autoAdd': 'Selbst eintragen', + 'mailInbox.settings.autoAddHint': + 'Eine sichere Buchung landet direkt in der Reise und lässt sich 7 Tage lang rückgängig machen. Aus, wartet jede auf dein OK.', + 'mailInbox.settings.upload': 'Mail-Datei hochladen', + 'mailInbox.settings.uploaded': 'Mail angekommen, sie wird gelesen', + 'mailInbox.settings.uploadDuplicate': 'Diese Mail ist schon da', + 'mailInbox.settings.uploadError': 'Die Datei konnte nicht als Mail gelesen werden', + 'mailInbox.settings.privacy': + 'Weitergeleitete Mails werden verschlüsselt höchstens 30 Tage aufbewahrt und dann gelöscht. Bitte keine Ausweis- oder Gesundheitsdokumente weiterleiten.', + 'mailInbox.trip.title': 'Oder per Mail weiterleiten', + 'mailInbox.trip.hint': 'Eine Buchungsmail an die Adresse dieser Reise landet ohne Raten genau hier.', + 'mailInbox.trip.create': 'Adresse für diese Reise anlegen', + 'mailInbox.fromMail': 'Aus Mail', + 'mailInbox.fromMailHint': 'Aus einer weitergeleiteten Buchungsmail eingetragen', +}; +export default mailInbox; diff --git a/shared/src/i18n/de/notif.ts b/shared/src/i18n/de/notif.ts index eeaf07f2c..a8e4eadee 100644 --- a/shared/src/i18n/de/notif.ts +++ b/shared/src/i18n/de/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} wurde in {trip} eingetragen', 'notif.mail_booking_added.shared': '{actor} hat {booking} aus einer weitergeleiteten Mail in {trip} eingetragen', 'notif.mail_booking_pending.title': 'Weitergeleitete Buchungen prüfen', - 'notif.mail_booking_pending.text': 'Weitergeleitete Buchungen warten auf dich: {count}', + 'notif.mail_booking_pending.text': '{count} weitergeleitete Buchungen warten auf dich', + 'notif.mail_booking_pending.text.one': '{count} weitergeleitete Buchung wartet auf dich', 'notif.mail_ingest_paused.title': 'Mail-Import pausiert', 'notif.mail_ingest_paused.text': 'Weitergeleitete Buchungsmails werden vorerst nicht angenommen. Deine Adressen bleiben und funktionieren wieder, sobald der Import zurück ist.', diff --git a/shared/src/i18n/de/reservations.ts b/shared/src/i18n/de/reservations.ts index 78661c5ea..c163ae8ac 100644 --- a/shared/src/i18n/de/reservations.ts +++ b/shared/src/i18n/de/reservations.ts @@ -38,7 +38,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Anschlussflug', 'reservations.layover.layover': 'Zwischenstopp', 'reservations.needsReview': 'Prüfen', - 'reservations.needsReviewHint': 'Flughafen konnte nicht automatisch erkannt werden — bitte Ort bestätigen.', + 'reservations.needsReviewHint': + 'Einige Angaben wurden automatisch eingetragen und konnten nicht sicher gelesen werden. Bitte prüfen.', + 'reservations.reviewConfirm': 'Passt so', + 'reservations.reviewConfirmHint': 'Klicken, um sie als geprüft zu markieren.', + 'reservations.reviewConfirmed': 'Als geprüft markiert', 'reservations.searchLocation': 'Bahnhof, Hafen, Adresse suchen…', 'reservations.meta.trainNumber': 'Zugnr.', 'reservations.meta.platform': 'Gleis', diff --git a/shared/src/i18n/de/settings.ts b/shared/src/i18n/de/settings.ts index cb798c059..298719823 100644 --- a/shared/src/i18n/de/settings.ts +++ b/shared/src/i18n/de/settings.ts @@ -117,6 +117,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Flugverspätungen', 'settings.notifyFlightLanded': 'Landungen', 'settings.notifyTripReminder': 'Trip-Erinnerungen', + 'settings.notifyMailBookingAdded': 'Buchungen aus Mails', + 'settings.notifyMailBookingPending': 'Weitergeleitete Buchungen zum Prüfen', + 'settings.notifyMailIngestPaused': 'Mail-Import pausiert', 'settings.notifyTodoDue': 'Aufgabe bald fällig', 'settings.notifyVacayInvite': 'Vacay Fusion-Einladungen', 'settings.notifyVacayShare': 'Vacay Kalender-Freigaben', diff --git a/shared/src/i18n/en/index.ts b/shared/src/i18n/en/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/en/index.ts +++ b/shared/src/i18n/en/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/en/mailAdmin.ts b/shared/src/i18n/en/mailAdmin.ts new file mode 100644 index 000000000..b58df41a7 --- /dev/null +++ b/shared/src/i18n/en/mailAdmin.ts @@ -0,0 +1,46 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Booking mail import', + 'mailAdmin.hint': 'Forwarded booking confirmations become bookings in the right trip.', + 'mailAdmin.enabled': 'Import booking mails', + 'mailAdmin.enabledHint': 'Gives everyone on this install a forwarding address. Off, no address takes mail.', + 'mailAdmin.transport.label': 'How mail arrives', + 'mailAdmin.transport.off': 'Upload only', + 'mailAdmin.transport.imap': 'IMAP mailbox', + 'mailAdmin.transport.webhook': 'Webhook', + 'mailAdmin.aiDaily': 'AI mails per person and day', + 'mailAdmin.aiDailyHint': 'Above it, only machine-readable booking data is read.', + 'mailAdmin.template': 'Address template', + 'mailAdmin.templateHint': 'The forwarding address, with {token} where each person’s code goes.', + 'mailAdmin.imap.host': 'Server', + 'mailAdmin.imap.port': 'Port', + 'mailAdmin.imap.tls': 'Encryption', + 'mailAdmin.imap.user': 'User', + 'mailAdmin.imap.pass': 'Password', + 'mailAdmin.imap.mailbox': 'Folder', + 'mailAdmin.imap.mailboxHint': 'Read mails move on to TREK/processed.', + 'mailAdmin.webhook.secret': 'Signing secret', + 'mailAdmin.webhook.secretHint': + 'At least 32 characters; the sender signs every mail with it. Empty switches the webhook off.', + 'mailAdmin.inboundUrl': 'Webhook address', + 'mailAdmin.inboundUrlHint': 'Your mail service posts each mail here, raw and signed.', + 'mailAdmin.copyUrl': 'Copy address', + 'mailAdmin.envHint': 'Environment variables (MAIL_INGEST_*) win over these fields.', + 'mailAdmin.loadError': 'The mail import settings could not be loaded.', + 'mailAdmin.problem.secretShort': 'The secret needs at least 32 characters.', + 'mailAdmin.problem.portInvalid': 'Enter a port between 1 and 65535.', + 'mailAdmin.problem.templateToken': 'The address needs {token} for each person’s code.', + 'mailAdmin.problem.dailyInvalid': 'Enter a whole number from 0 to 1000.', + 'mailAdmin.status.title': 'Status', + 'mailAdmin.status.lastRead': 'Last read {time}', + 'mailAdmin.status.never': 'never', + 'mailAdmin.status.mails': 'Mails, 24 h', + 'mailAdmin.status.added': 'Added on their own', + 'mailAdmin.status.waiting': 'Waiting', + 'mailAdmin.status.failed': 'Failed', + 'mailAdmin.test.run': 'Test connection', + 'mailAdmin.test.ok': 'Connected', + 'mailAdmin.test.failed': 'No connection', +}; +export default mailAdmin; diff --git a/shared/src/i18n/en/mailInbox.ts b/shared/src/i18n/en/mailInbox.ts new file mode 100644 index 000000000..54d36cdd9 --- /dev/null +++ b/shared/src/i18n/en/mailInbox.ts @@ -0,0 +1,97 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Pending bookings', + 'mailInbox.pendingCount': '{count} bookings wait for you', + 'mailInbox.pendingCount.one': '{count} booking waits for you', + 'mailInbox.empty': 'Nothing waiting', + 'mailInbox.emptyHint': 'Bookings TREK could not add on its own show up here, with what you can do about each.', + 'mailInbox.untitled': 'Booking mail', + 'mailInbox.showMail': 'Show mail', + 'mailInbox.backToList': 'Back to the list', + 'mailInbox.loadMore': 'Show more', + 'mailInbox.pickTrip': 'Which trip?', + 'mailInbox.added': 'Booking added', + 'mailInbox.filed': 'Saved to the trip’s files', + 'mailInbox.discarded': 'Discarded', + 'mailInbox.actionError': 'That did not work. Try again in a moment.', + 'mailInbox.previewError': 'The mail could not be shown.', + 'mailInbox.action.add': 'Add', + 'mailInbox.action.choose': 'Add to a trip', + 'mailInbox.action.file': 'Save as a document', + 'mailInbox.action.manual': 'Fill in yourself', + 'mailInbox.action.discard': 'Discard', + 'mailInbox.reason.forwardingConfirmation': + 'Your mail provider asks to confirm the forwarding. Enter this code there.', + 'mailInbox.reason.newsletter': 'A newsletter, not a booking', + 'mailInbox.reason.autoReply': 'An automatic reply', + 'mailInbox.reason.emptyMail': 'The mail has no text or attachment to read', + 'mailInbox.reason.mailUnreadable': 'The mail could not be read', + 'mailInbox.reason.notABooking': 'No booking found in this mail', + 'mailInbox.reason.tooLarge': 'The mail is too large to read', + 'mailInbox.reason.addressInvalid': 'It came in on an address that no longer works', + 'mailInbox.reason.addressDailyLimit': 'Your address took all its mails for today', + 'mailInbox.reason.processingFailed': 'Reading it failed several times', + 'mailInbox.reason.aiQuotaExhausted': 'The AI allowance of this period is used up', + 'mailInbox.reason.aiDisabled': 'Read without AI, which found no booking', + 'mailInbox.reason.aiUserCap': 'Your daily limit of AI-read mails is reached', + 'mailInbox.reason.aiProviderError': 'The AI could not read it right now', + 'mailInbox.reason.noTripInRange': 'No trip covers these dates', + 'mailInbox.reason.tripAmbiguous': 'More than one trip could be meant', + 'mailInbox.reason.tripWithoutDates': 'The trip has no dates to check against', + 'mailInbox.reason.noEditPermission': 'You may not add bookings to that trip', + 'mailInbox.reason.demoMode': 'The demo adds no bookings', + 'mailInbox.reason.outsideTripDates': 'A date lies outside the trip', + 'mailInbox.reason.sourceMismatch': 'The mail’s details disagree with each other', + 'mailInbox.reason.implausible': 'A value cannot be right', + 'mailInbox.reason.notInText': 'A value read is not in the mail', + 'mailInbox.reason.imageOnly': 'Read from an image alone, so it needs your look', + 'mailInbox.reason.missingCriticalField': 'Type, date or route is missing', + 'mailInbox.reason.missingConfirmation': 'No booking reference found', + 'mailInbox.reason.tooManyBookings': 'More bookings in one mail than are added on their own', + 'mailInbox.reason.changeOrCancellation': 'A change or cancellation of an earlier booking', + 'mailInbox.reason.cancelled': 'The booking is cancelled', + 'mailInbox.reason.possibleDuplicate': 'The trip may hold this booking already', + 'mailInbox.reason.changeAvailable': 'The trip holds this booking, and the mail changes it', + 'mailInbox.reason.conflict': 'The trip holds this booking with other details', + 'mailInbox.reason.askFirst': 'You asked to confirm each booking', + 'mailInbox.settings.title': 'Booking mails', + 'mailInbox.settings.on': 'On', + 'mailInbox.settings.off': 'Off', + 'mailInbox.settings.yourAddress': 'Your address', + 'mailInbox.settings.copy': 'Copy address', + 'mailInbox.settings.copyError': 'Could not copy', + 'mailInbox.settings.renew': 'New address', + 'mailInbox.settings.renewConfirm': 'Tap again: the old address stops working', + 'mailInbox.settings.renewed': 'Your new address is ready', + 'mailInbox.settings.saveError': 'Could not save', + 'mailInbox.settings.addressHint': + 'Forward booking confirmations to this address and TREK adds them to the right trip. Keep it to yourself like a password.', + 'mailInbox.settings.howTo': 'Forward automatically', + 'mailInbox.settings.howToGmail': + 'Gmail: Settings, Forwarding and POP/IMAP, Add a forwarding address. Gmail sends a code, which shows up under Pending bookings. Then add a filter for booking senders that forwards to TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Settings, Mail, Rules, add a rule that forwards booking mails. Some work accounts block automatic forwarding; forward by hand there.', + 'mailInbox.settings.howToManual': 'By hand: forward any booking mail, attachments included.', + 'mailInbox.settings.enabled': 'Import booking mails', + 'mailInbox.settings.enabledHint': 'Off, mails to your address are not read.', + 'mailInbox.settings.ai': 'Read with AI', + 'mailInbox.settings.aiHint': + 'Reads any booking mail and finds its trip. The mail and the dates and destinations of your trips go to the AI provider.', + 'mailInbox.settings.aiUnavailable': 'Not available here: only mails with machine-readable booking data are read.', + 'mailInbox.settings.autoAdd': 'Add on its own', + 'mailInbox.settings.autoAddHint': + 'A booking TREK is sure about goes straight into its trip, and you can undo it for 7 days. Off, each one waits for your OK.', + 'mailInbox.settings.upload': 'Upload a mail file', + 'mailInbox.settings.uploaded': 'Mail received, it is being read', + 'mailInbox.settings.uploadDuplicate': 'This mail is in already', + 'mailInbox.settings.uploadError': 'The file could not be read as a mail', + 'mailInbox.settings.privacy': + 'Forwarded mails are kept encrypted for at most 30 days and then deleted. Please do not forward ID or health documents.', + 'mailInbox.trip.title': 'Or forward by mail', + 'mailInbox.trip.hint': 'A booking mail sent to this trip’s address lands here without any guessing.', + 'mailInbox.trip.create': 'Create an address for this trip', + 'mailInbox.fromMail': 'From mail', + 'mailInbox.fromMailHint': 'Added from a forwarded booking mail', +}; +export default mailInbox; diff --git a/shared/src/i18n/en/notif.ts b/shared/src/i18n/en/notif.ts index c1f30f5c1..555978e0f 100644 --- a/shared/src/i18n/en/notif.ts +++ b/shared/src/i18n/en/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} was added to {trip}', 'notif.mail_booking_added.shared': '{actor} added {booking} to {trip} from a forwarded mail', 'notif.mail_booking_pending.title': 'Forwarded bookings to review', - 'notif.mail_booking_pending.text': 'Forwarded bookings waiting for you: {count}', + 'notif.mail_booking_pending.text': '{count} forwarded bookings are waiting for you', + 'notif.mail_booking_pending.text.one': '{count} forwarded booking is waiting for you', 'notif.mail_ingest_paused.title': 'Mail import paused', 'notif.mail_ingest_paused.text': 'Forwarded booking mails are not taken in for now. Your addresses stay and work again once the import is back.', diff --git a/shared/src/i18n/en/reservations.ts b/shared/src/i18n/en/reservations.ts index 67a4fff77..6c89233c8 100644 --- a/shared/src/i18n/en/reservations.ts +++ b/shared/src/i18n/en/reservations.ts @@ -37,7 +37,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Connection', 'reservations.layover.layover': 'Layover', 'reservations.needsReview': 'Review', - 'reservations.needsReviewHint': 'Airport could not be matched automatically — please confirm the location.', + 'reservations.needsReviewHint': + 'Some details were filled in automatically and could not be read for sure. Please check them.', + 'reservations.reviewConfirm': 'Looks right', + 'reservations.reviewConfirmHint': 'Click to mark it as checked.', + 'reservations.reviewConfirmed': 'Marked as checked', 'reservations.searchLocation': 'Search station, port, address…', 'reservations.meta.trainNumber': 'Train No.', 'reservations.meta.platform': 'Platform', diff --git a/shared/src/i18n/en/settings.ts b/shared/src/i18n/en/settings.ts index 3d7195da2..528c65aea 100644 --- a/shared/src/i18n/en/settings.ts +++ b/shared/src/i18n/en/settings.ts @@ -126,6 +126,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Flight delays', 'settings.notifyFlightLanded': 'Flight landings', 'settings.notifyTripReminder': 'Trip reminders', + 'settings.notifyMailBookingAdded': 'Bookings added from mails', + 'settings.notifyMailBookingPending': 'Forwarded bookings to review', + 'settings.notifyMailIngestPaused': 'Mail import paused', 'settings.notifyTodoDue': 'Todo due soon', 'settings.notifyVacayInvite': 'Vacay fusion invitations', 'settings.notifyVacayShare': 'Vacay calendar shares', diff --git a/shared/src/i18n/es/index.ts b/shared/src/i18n/es/index.ts index 6525ebe9c..ccf96883c 100644 --- a/shared/src/i18n/es/index.ts +++ b/shared/src/i18n/es/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/es/mailAdmin.ts b/shared/src/i18n/es/mailAdmin.ts new file mode 100644 index 000000000..91e8ee273 --- /dev/null +++ b/shared/src/i18n/es/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Importación de correos de reserva', + 'mailAdmin.hint': 'Las confirmaciones de reserva reenviadas se convierten en reservas del viaje correcto.', + 'mailAdmin.enabled': 'Importar correos de reserva', + 'mailAdmin.enabledHint': + 'Da a todos los usuarios de esta instalación una dirección de reenvío. Desactivado, ninguna dirección recibe correo.', + 'mailAdmin.transport.label': 'Cómo llega el correo', + 'mailAdmin.transport.off': 'Solo subida de archivos', + 'mailAdmin.transport.imap': 'Buzón IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Correos con IA por persona y día', + 'mailAdmin.aiDailyHint': 'Por encima de ese número, solo se leen datos de reserva legibles por máquina.', + 'mailAdmin.template': 'Plantilla de dirección', + 'mailAdmin.templateHint': 'La dirección de reenvío, con {token} donde va el código de cada persona.', + 'mailAdmin.imap.host': 'Servidor', + 'mailAdmin.imap.port': 'Puerto', + 'mailAdmin.imap.tls': 'Cifrado', + 'mailAdmin.imap.user': 'Usuario', + 'mailAdmin.imap.pass': 'Contraseña', + 'mailAdmin.imap.mailbox': 'Carpeta', + 'mailAdmin.imap.mailboxHint': 'Los correos leídos se mueven a TREK/processed.', + 'mailAdmin.webhook.secret': 'Secreto de firma', + 'mailAdmin.webhook.secretHint': + 'Al menos 32 caracteres; el remitente firma cada correo con él. Vacío, el webhook se desactiva.', + 'mailAdmin.inboundUrl': 'Dirección del webhook', + 'mailAdmin.inboundUrlHint': 'Tu servicio de correo envía aquí cada correo, en bruto y firmado.', + 'mailAdmin.copyUrl': 'Copiar dirección', + 'mailAdmin.envHint': 'Las variables de entorno (MAIL_INGEST_*) tienen prioridad sobre estos campos.', + 'mailAdmin.loadError': 'No se pudo cargar la configuración de importación de correo.', + 'mailAdmin.problem.secretShort': 'El secreto necesita al menos 32 caracteres.', + 'mailAdmin.problem.portInvalid': 'Introduce un puerto entre 1 y 65535.', + 'mailAdmin.problem.templateToken': 'La dirección necesita {token} para el código de cada persona.', + 'mailAdmin.problem.dailyInvalid': 'Introduce un número entero de 0 a 1000.', + 'mailAdmin.status.title': 'Estado', + 'mailAdmin.status.lastRead': 'Última lectura {time}', + 'mailAdmin.status.never': 'nunca', + 'mailAdmin.status.mails': 'Correos, 24 h', + 'mailAdmin.status.added': 'Añadidos automáticamente', + 'mailAdmin.status.waiting': 'En espera', + 'mailAdmin.status.failed': 'Fallidos', + 'mailAdmin.test.run': 'Probar conexión', + 'mailAdmin.test.ok': 'Conectado', + 'mailAdmin.test.failed': 'Sin conexión', +}; +export default mailAdmin; diff --git a/shared/src/i18n/es/mailInbox.ts b/shared/src/i18n/es/mailInbox.ts new file mode 100644 index 000000000..013af4e89 --- /dev/null +++ b/shared/src/i18n/es/mailInbox.ts @@ -0,0 +1,99 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Reservas pendientes', + 'mailInbox.pendingCount': '{count} reservas te esperan', + 'mailInbox.pendingCount.one': '{count} reserva te espera', + 'mailInbox.empty': 'Nada pendiente', + 'mailInbox.emptyHint': + 'Las reservas que TREK no pudo añadir por sí solo aparecen aquí, con lo que puedes hacer en cada caso.', + 'mailInbox.untitled': 'Correo de reserva', + 'mailInbox.showMail': 'Mostrar correo', + 'mailInbox.backToList': 'Volver a la lista', + 'mailInbox.loadMore': 'Mostrar más', + 'mailInbox.pickTrip': '¿Qué viaje?', + 'mailInbox.added': 'Reserva añadida', + 'mailInbox.filed': 'Guardado en los archivos del viaje', + 'mailInbox.discarded': 'Descartado', + 'mailInbox.actionError': 'No ha funcionado. Inténtalo de nuevo en un momento.', + 'mailInbox.previewError': 'No se pudo mostrar el correo.', + 'mailInbox.action.add': 'Añadir', + 'mailInbox.action.choose': 'Añadir a un viaje', + 'mailInbox.action.file': 'Guardar como documento', + 'mailInbox.action.manual': 'Rellenar tú mismo', + 'mailInbox.action.discard': 'Descartar', + 'mailInbox.reason.forwardingConfirmation': + 'Tu proveedor de correo pide confirmar el reenvío. Introduce este código allí.', + 'mailInbox.reason.newsletter': 'Un boletín, no una reserva', + 'mailInbox.reason.autoReply': 'Una respuesta automática', + 'mailInbox.reason.emptyMail': 'El correo no tiene texto ni adjunto que leer', + 'mailInbox.reason.mailUnreadable': 'No se pudo leer el correo', + 'mailInbox.reason.notABooking': 'No se encontró ninguna reserva en este correo', + 'mailInbox.reason.tooLarge': 'El correo es demasiado grande para leerlo', + 'mailInbox.reason.addressInvalid': 'Llegó a una dirección que ya no funciona', + 'mailInbox.reason.addressDailyLimit': 'Tu dirección ya ha recibido todos los correos de hoy', + 'mailInbox.reason.processingFailed': 'La lectura falló varias veces', + 'mailInbox.reason.aiQuotaExhausted': 'Se ha agotado la cuota de IA de este periodo', + 'mailInbox.reason.aiDisabled': 'Leído sin IA, que no encontró ninguna reserva', + 'mailInbox.reason.aiUserCap': 'Has alcanzado tu límite diario de correos leídos con IA', + 'mailInbox.reason.aiProviderError': 'La IA no pudo leerlo ahora', + 'mailInbox.reason.noTripInRange': 'Ningún viaje cubre estas fechas', + 'mailInbox.reason.tripAmbiguous': 'Podría tratarse de más de un viaje', + 'mailInbox.reason.tripWithoutDates': 'El viaje no tiene fechas con las que comparar', + 'mailInbox.reason.noEditPermission': 'No puedes añadir reservas a ese viaje', + 'mailInbox.reason.demoMode': 'La demo no añade reservas', + 'mailInbox.reason.outsideTripDates': 'Una fecha queda fuera del viaje', + 'mailInbox.reason.sourceMismatch': 'Los datos del correo no coinciden entre sí', + 'mailInbox.reason.implausible': 'Un valor no puede ser correcto', + 'mailInbox.reason.notInText': 'Un valor leído no aparece en el correo', + 'mailInbox.reason.imageOnly': 'Leído solo a partir de una imagen, así que necesita tu revisión', + 'mailInbox.reason.missingCriticalField': 'Falta el tipo, la fecha o el trayecto', + 'mailInbox.reason.missingConfirmation': 'No se encontró ningún localizador de reserva', + 'mailInbox.reason.tooManyBookings': 'Hay más reservas en un solo correo de las que se añaden automáticamente', + 'mailInbox.reason.changeOrCancellation': 'Un cambio o una cancelación de una reserva anterior', + 'mailInbox.reason.cancelled': 'La reserva está cancelada', + 'mailInbox.reason.possibleDuplicate': 'Puede que el viaje ya tenga esta reserva', + 'mailInbox.reason.changeAvailable': 'El viaje ya tiene esta reserva, y el correo la modifica', + 'mailInbox.reason.conflict': 'El viaje tiene esta reserva con otros datos', + 'mailInbox.reason.askFirst': 'Has pedido confirmar cada reserva', + 'mailInbox.settings.title': 'Correos de reserva', + 'mailInbox.settings.on': 'Activado', + 'mailInbox.settings.off': 'Desactivado', + 'mailInbox.settings.yourAddress': 'Tu dirección', + 'mailInbox.settings.copy': 'Copiar dirección', + 'mailInbox.settings.copyError': 'No se pudo copiar', + 'mailInbox.settings.renew': 'Nueva dirección', + 'mailInbox.settings.renewConfirm': 'Toca de nuevo: la dirección anterior dejará de funcionar', + 'mailInbox.settings.renewed': 'Tu nueva dirección está lista', + 'mailInbox.settings.saveError': 'No se pudo guardar', + 'mailInbox.settings.addressHint': + 'Reenvía las confirmaciones de reserva a esta dirección y TREK las añade al viaje correcto. Mantenla en privado, como una contraseña.', + 'mailInbox.settings.howTo': 'Reenviar automáticamente', + 'mailInbox.settings.howToGmail': + 'Gmail: Configuración, Reenvío y correo POP/IMAP, Añadir una dirección de reenvío. Gmail envía un código, que aparece en Reservas pendientes. Después añade un filtro para los remitentes de reservas que reenvíe a TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Configuración, Correo, Reglas, añade una regla que reenvíe los correos de reserva. Algunas cuentas de trabajo bloquean el reenvío automático; allí reenvía a mano.', + 'mailInbox.settings.howToManual': 'A mano: reenvía cualquier correo de reserva, con sus adjuntos.', + 'mailInbox.settings.enabled': 'Importar correos de reserva', + 'mailInbox.settings.enabledHint': 'Desactivado, los correos enviados a tu dirección no se leen.', + 'mailInbox.settings.ai': 'Leer con IA', + 'mailInbox.settings.aiHint': + 'Lee cualquier correo de reserva y encuentra su viaje. El correo y las fechas y destinos de tus viajes se envían al proveedor de IA.', + 'mailInbox.settings.aiUnavailable': + 'No disponible aquí: solo se leen correos con datos de reserva legibles por máquina.', + 'mailInbox.settings.autoAdd': 'Añadir automáticamente', + 'mailInbox.settings.autoAddHint': + 'Una reserva de la que TREK está seguro va directa a su viaje, y puedes deshacerlo durante 7 días. Desactivado, cada una espera tu visto bueno.', + 'mailInbox.settings.upload': 'Subir un archivo de correo', + 'mailInbox.settings.uploaded': 'Correo recibido, se está leyendo', + 'mailInbox.settings.uploadDuplicate': 'Este correo ya está importado', + 'mailInbox.settings.uploadError': 'No se pudo leer el archivo como correo', + 'mailInbox.settings.privacy': + 'Los correos reenviados se guardan cifrados durante un máximo de 30 días y luego se eliminan. Por favor, no reenvíes documentos de identidad ni de salud.', + 'mailInbox.trip.title': 'O reenvía por correo', + 'mailInbox.trip.hint': 'Un correo de reserva enviado a la dirección de este viaje llega aquí sin adivinar nada.', + 'mailInbox.trip.create': 'Crear una dirección para este viaje', + 'mailInbox.fromMail': 'Desde correo', + 'mailInbox.fromMailHint': 'Añadido desde un correo de reserva reenviado', +}; +export default mailInbox; diff --git a/shared/src/i18n/es/notif.ts b/shared/src/i18n/es/notif.ts index 4b502b3ad..84338dcc5 100644 --- a/shared/src/i18n/es/notif.ts +++ b/shared/src/i18n/es/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} se añadió a {trip}', 'notif.mail_booking_added.shared': '{actor} añadió {booking} a {trip} desde un correo reenviado', 'notif.mail_booking_pending.title': 'Reservas reenviadas por revisar', - 'notif.mail_booking_pending.text': 'Reservas reenviadas pendientes: {count}', + 'notif.mail_booking_pending.text': '{count} reservas reenviadas te esperan', + 'notif.mail_booking_pending.text.one': '{count} reserva reenviada te espera', 'notif.mail_ingest_paused.title': 'Importación de correos en pausa', 'notif.mail_ingest_paused.text': 'Por ahora no se aceptan los correos de reserva reenviados. Tus direcciones se mantienen y volverán a funcionar cuando vuelva la importación.', diff --git a/shared/src/i18n/es/reservations.ts b/shared/src/i18n/es/reservations.ts index e580308eb..55207ed14 100644 --- a/shared/src/i18n/es/reservations.ts +++ b/shared/src/i18n/es/reservations.ts @@ -119,7 +119,10 @@ const reservations: TranslationStrings = { 'reservations.layover.layover': 'Escala', 'reservations.needsReview': 'Revisar', 'reservations.needsReviewHint': - 'No se pudo identificar el aeropuerto automáticamente — por favor confirma la ubicación.', + 'Algunos datos se rellenaron automáticamente y no se pudieron leer con seguridad. Por favor, revísalos.', + 'reservations.reviewConfirm': 'Es correcto', + 'reservations.reviewConfirmHint': 'Haz clic para marcarlo como revisado.', + 'reservations.reviewConfirmed': 'Marcado como revisado', 'reservations.searchLocation': 'Buscar estación, puerto, dirección...', 'reservations.meta.trainNumber': 'N° de tren', 'reservations.meta.platform': 'Andén', diff --git a/shared/src/i18n/es/settings.ts b/shared/src/i18n/es/settings.ts index 21cc33fc1..3ca1f2f54 100644 --- a/shared/src/i18n/es/settings.ts +++ b/shared/src/i18n/es/settings.ts @@ -116,6 +116,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Retrasos de vuelos', 'settings.notifyFlightLanded': 'Aterrizajes', 'settings.notifyTripReminder': 'Recordatorios de viaje', + 'settings.notifyMailBookingAdded': 'Reservas añadidas desde correos', + 'settings.notifyMailBookingPending': 'Reservas reenviadas por revisar', + 'settings.notifyMailIngestPaused': 'Importación de correo en pausa', 'settings.notifyTodoDue': 'Tarea próxima', 'settings.notifyVacayInvite': 'Invitaciones de fusión Vacay', 'settings.notifyVacayShare': 'Calendarios Vacay compartidos', diff --git a/shared/src/i18n/et/index.ts b/shared/src/i18n/et/index.ts index bb9ce471c..74f0d5974 100644 --- a/shared/src/i18n/et/index.ts +++ b/shared/src/i18n/et/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/et/mailAdmin.ts b/shared/src/i18n/et/mailAdmin.ts new file mode 100644 index 000000000..d8c81e08d --- /dev/null +++ b/shared/src/i18n/et/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Broneeringukirjade import', + 'mailAdmin.hint': 'Edastatud broneeringute kinnitustest saavad broneeringud õigel reisil.', + 'mailAdmin.enabled': 'Impordi broneeringukirjad', + 'mailAdmin.enabledHint': + 'Annab igaühele selles paigalduses edastusaadressi. Väljalülitatuna ei võta ükski aadress kirju vastu.', + 'mailAdmin.transport.label': 'Kuidas kiri saabub', + 'mailAdmin.transport.off': 'Ainult üleslaadimine', + 'mailAdmin.transport.imap': 'IMAP-postkast', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Tehisintellekti kirju inimese kohta päevas', + 'mailAdmin.aiDailyHint': 'Sellest üle loetakse ainult masinloetavaid broneeringuandmeid.', + 'mailAdmin.template': 'Aadressi mall', + 'mailAdmin.templateHint': 'Edastusaadress, kus {token} asub iga inimese koodi kohal.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Krüpteerimine', + 'mailAdmin.imap.user': 'Kasutaja', + 'mailAdmin.imap.pass': 'Parool', + 'mailAdmin.imap.mailbox': 'Kaust', + 'mailAdmin.imap.mailboxHint': 'Loetud kirjad liiguvad kausta TREK/processed.', + 'mailAdmin.webhook.secret': 'Allkirjastamise saladus', + 'mailAdmin.webhook.secretHint': + 'Vähemalt 32 märki; saatja allkirjastab sellega iga kirja. Tühi väli lülitab webhooki välja.', + 'mailAdmin.inboundUrl': 'Webhooki aadress', + 'mailAdmin.inboundUrlHint': 'Sinu e-posti teenus postitab iga kirja siia, toorelt ja allkirjastatult.', + 'mailAdmin.copyUrl': 'Kopeeri aadress', + 'mailAdmin.envHint': 'Keskkonnamuutujad (MAIL_INGEST_*) on nendest väljadest tähtsamad.', + 'mailAdmin.loadError': 'Kirjaimpordi seadeid ei saanud laadida.', + 'mailAdmin.problem.secretShort': 'Saladus peab olema vähemalt 32 märki.', + 'mailAdmin.problem.portInvalid': 'Sisesta port vahemikus 1 kuni 65535.', + 'mailAdmin.problem.templateToken': 'Aadressis peab iga inimese koodi jaoks olema {token}.', + 'mailAdmin.problem.dailyInvalid': 'Sisesta täisarv vahemikus 0 kuni 1000.', + 'mailAdmin.status.title': 'Olek', + 'mailAdmin.status.lastRead': 'Viimati loetud {time}', + 'mailAdmin.status.never': 'mitte kunagi', + 'mailAdmin.status.mails': 'Kirjad, 24 t', + 'mailAdmin.status.added': 'Ise lisatud', + 'mailAdmin.status.waiting': 'Ootel', + 'mailAdmin.status.failed': 'Ebaõnnestunud', + 'mailAdmin.test.run': 'Testi ühendust', + 'mailAdmin.test.ok': 'Ühendatud', + 'mailAdmin.test.failed': 'Ühendus puudub', +}; +export default mailAdmin; diff --git a/shared/src/i18n/et/mailInbox.ts b/shared/src/i18n/et/mailInbox.ts new file mode 100644 index 000000000..d843909b0 --- /dev/null +++ b/shared/src/i18n/et/mailInbox.ts @@ -0,0 +1,99 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Ootel broneeringud', + 'mailInbox.pendingCount': '{count} broneeringut ootab sind', + 'mailInbox.pendingCount.one': '{count} broneering ootab sind', + 'mailInbox.empty': 'Midagi ei oota', + 'mailInbox.emptyHint': + 'Broneeringud, mida TREK ise lisada ei saanud, ilmuvad siia koos võimalustega, mida igaühega teha.', + 'mailInbox.untitled': 'Broneeringukiri', + 'mailInbox.showMail': 'Näita kirja', + 'mailInbox.backToList': 'Tagasi nimekirja', + 'mailInbox.loadMore': 'Näita rohkem', + 'mailInbox.pickTrip': 'Milline reis?', + 'mailInbox.added': 'Broneering lisatud', + 'mailInbox.filed': 'Salvestatud reisi failidesse', + 'mailInbox.discarded': 'Kustutatud', + 'mailInbox.actionError': 'See ei õnnestunud. Proovi hetke pärast uuesti.', + 'mailInbox.previewError': 'Kirja ei saanud kuvada.', + 'mailInbox.action.add': 'Lisa', + 'mailInbox.action.choose': 'Lisa reisile', + 'mailInbox.action.file': 'Salvesta dokumendina', + 'mailInbox.action.manual': 'Täida ise', + 'mailInbox.action.discard': 'Kustuta', + 'mailInbox.reason.forwardingConfirmation': + 'Sinu e-posti teenusepakkuja küsib edastamise kinnitust. Sisesta see kood seal.', + 'mailInbox.reason.newsletter': 'Uudiskiri, mitte broneering', + 'mailInbox.reason.autoReply': 'Automaatvastus', + 'mailInbox.reason.emptyMail': 'Kirjas pole loetavat teksti ega manust', + 'mailInbox.reason.mailUnreadable': 'Kirja ei saanud lugeda', + 'mailInbox.reason.notABooking': 'Sellest kirjast ei leitud broneeringut', + 'mailInbox.reason.tooLarge': 'Kiri on lugemiseks liiga suur', + 'mailInbox.reason.addressInvalid': 'See saabus aadressile, mis enam ei tööta', + 'mailInbox.reason.addressDailyLimit': 'Sinu aadress on tänase kirjade piiri täis saanud', + 'mailInbox.reason.processingFailed': 'Lugemine ebaõnnestus mitu korda', + 'mailInbox.reason.aiQuotaExhausted': 'Selle perioodi tehisintellekti kvoot on otsas', + 'mailInbox.reason.aiDisabled': 'Loetud ilma tehisintellektita, broneeringut ei leitud', + 'mailInbox.reason.aiUserCap': 'Oled jõudnud tehisintellektiga loetavate kirjade päevapiirini', + 'mailInbox.reason.aiProviderError': 'Tehisintellekt ei suutnud seda praegu lugeda', + 'mailInbox.reason.noTripInRange': 'Ükski reis ei hõlma neid kuupäevi', + 'mailInbox.reason.tripAmbiguous': 'Silmas võib olla peetud mitut reisi', + 'mailInbox.reason.tripWithoutDates': 'Reisil pole kuupäevi, millega võrrelda', + 'mailInbox.reason.noEditPermission': 'Sul pole õigust sellele reisile broneeringuid lisada', + 'mailInbox.reason.demoMode': 'Demo ei lisa broneeringuid', + 'mailInbox.reason.outsideTripDates': 'Üks kuupäev jääb reisist välja', + 'mailInbox.reason.sourceMismatch': 'Kirja andmed on omavahel vastuolus', + 'mailInbox.reason.implausible': 'Üks väärtus ei saa õige olla', + 'mailInbox.reason.notInText': 'Loetud väärtust kirjas pole', + 'mailInbox.reason.imageOnly': 'Loetud ainult pildilt, seega vaata see üle', + 'mailInbox.reason.missingCriticalField': 'Tüüp, kuupäev või marsruut puudub', + 'mailInbox.reason.missingConfirmation': 'Broneeringu numbrit ei leitud', + 'mailInbox.reason.tooManyBookings': 'Ühes kirjas on rohkem broneeringuid, kui ise lisatakse', + 'mailInbox.reason.changeOrCancellation': 'Varasema broneeringu muudatus või tühistamine', + 'mailInbox.reason.cancelled': 'Broneering on tühistatud', + 'mailInbox.reason.possibleDuplicate': 'Reisil võib see broneering juba olemas olla', + 'mailInbox.reason.changeAvailable': 'Reisil on see broneering ja kiri muudab seda', + 'mailInbox.reason.conflict': 'Reisil on see broneering teiste andmetega', + 'mailInbox.reason.askFirst': 'Palusid iga broneering kinnitada', + 'mailInbox.settings.title': 'Broneeringukirjad', + 'mailInbox.settings.on': 'Sees', + 'mailInbox.settings.off': 'Väljas', + 'mailInbox.settings.yourAddress': 'Sinu aadress', + 'mailInbox.settings.copy': 'Kopeeri aadress', + 'mailInbox.settings.copyError': 'Kopeerimine ebaõnnestus', + 'mailInbox.settings.renew': 'Uus aadress', + 'mailInbox.settings.renewConfirm': 'Puuduta uuesti: vana aadress lakkab töötamast', + 'mailInbox.settings.renewed': 'Sinu uus aadress on valmis', + 'mailInbox.settings.saveError': 'Salvestamine ebaõnnestus', + 'mailInbox.settings.addressHint': + 'Edasta broneeringute kinnitused sellele aadressile ja TREK lisab need õigele reisile. Hoia seda enda teada nagu parooli.', + 'mailInbox.settings.howTo': 'Edasta automaatselt', + 'mailInbox.settings.howToGmail': + 'Gmail: Seaded, Edastamine ja POP/IMAP, Lisa edastusaadress. Gmail saadab koodi, mis ilmub jaotisse Ootel broneeringud. Seejärel lisa broneeringute saatjatele filter, mis edastab TREKi.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Seaded, Meil, Reeglid, lisa reegel, mis edastab broneeringukirjad. Mõned töökontod blokeerivad automaatse edastamise; edasta seal käsitsi.', + 'mailInbox.settings.howToManual': 'Käsitsi: edasta mis tahes broneeringukiri koos manustega.', + 'mailInbox.settings.enabled': 'Impordi broneeringukirjad', + 'mailInbox.settings.enabledHint': 'Väljalülitatuna sinu aadressile saadetud kirju ei loeta.', + 'mailInbox.settings.ai': 'Loe tehisintellektiga', + 'mailInbox.settings.aiHint': + 'Loeb mis tahes broneeringukirja ja leiab selle reisi. Kiri ning sinu reiside kuupäevad ja sihtkohad lähevad tehisintellekti pakkujale.', + 'mailInbox.settings.aiUnavailable': + 'Siin pole saadaval: loetakse ainult kirju, mis sisaldavad masinloetavaid broneeringuandmeid.', + 'mailInbox.settings.autoAdd': 'Lisa ise', + 'mailInbox.settings.autoAddHint': + 'Broneering, milles TREK on kindel, läheb otse oma reisile ja seda saab 7 päeva jooksul tagasi võtta. Väljalülitatuna ootab iga broneering sinu kinnitust.', + 'mailInbox.settings.upload': 'Laadi üles kirjafail', + 'mailInbox.settings.uploaded': 'Kiri vastu võetud, seda loetakse', + 'mailInbox.settings.uploadDuplicate': 'See kiri on juba olemas', + 'mailInbox.settings.uploadError': 'Faili ei saanud kirjana lugeda', + 'mailInbox.settings.privacy': + 'Edastatud kirju hoitakse krüpteeritult kuni 30 päeva ja seejärel kustutatakse. Palun ära edasta isikut tõendavaid ega terviseandmeid sisaldavaid dokumente.', + 'mailInbox.trip.title': 'Või edasta e-kirjaga', + 'mailInbox.trip.hint': 'Selle reisi aadressile saadetud broneeringukiri jõuab siia ilma aimamiseta.', + 'mailInbox.trip.create': 'Loo sellele reisile aadress', + 'mailInbox.fromMail': 'Kirjast', + 'mailInbox.fromMailHint': 'Lisatud edastatud broneeringukirjast', +}; +export default mailInbox; diff --git a/shared/src/i18n/et/notif.ts b/shared/src/i18n/et/notif.ts index 75a8c6b4a..03f4fc071 100644 --- a/shared/src/i18n/et/notif.ts +++ b/shared/src/i18n/et/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} lisati reisile {trip}', 'notif.mail_booking_added.shared': '{actor} lisas edastatud kirjast {booking} reisile {trip}', 'notif.mail_booking_pending.title': 'Ülevaatamist ootavad edastatud broneeringud', - 'notif.mail_booking_pending.text': 'Sind ootavad edastatud broneeringud: {count}', + 'notif.mail_booking_pending.text': '{count} edastatud broneeringut ootab sind', + 'notif.mail_booking_pending.text.one': '{count} edastatud broneering ootab sind', 'notif.mail_ingest_paused.title': 'Kirjade import on peatatud', 'notif.mail_ingest_paused.text': 'Edastatud broneeringukirju praegu vastu ei võeta. Sinu aadressid jäävad alles ja töötavad uuesti, kui import tagasi on.', diff --git a/shared/src/i18n/et/reservations.ts b/shared/src/i18n/et/reservations.ts index 2f0f2e1fc..1e34d6d05 100644 --- a/shared/src/i18n/et/reservations.ts +++ b/shared/src/i18n/et/reservations.ts @@ -37,7 +37,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Ühendus', 'reservations.layover.layover': 'Vahepeatus', 'reservations.needsReview': 'Vaata üle', - 'reservations.needsReviewHint': 'Lennujaama ei õnnestunud automaatselt tuvastada — kinnita asukoht.', + 'reservations.needsReviewHint': + 'Osa andmeid täideti automaatselt ja neid ei saanud kindlalt lugeda. Palun kontrolli need üle.', + 'reservations.reviewConfirm': 'Tundub õige', + 'reservations.reviewConfirmHint': 'Klõpsa, et märkida kontrollituks.', + 'reservations.reviewConfirmed': 'Märgitud kontrollituks', 'reservations.searchLocation': 'Otsi jaama, sadamat, aadressi…', 'reservations.meta.trainNumber': 'Rongi nr', 'reservations.meta.platform': 'Platvorm', diff --git a/shared/src/i18n/et/settings.ts b/shared/src/i18n/et/settings.ts index d95988661..02aa53f70 100644 --- a/shared/src/i18n/et/settings.ts +++ b/shared/src/i18n/et/settings.ts @@ -131,6 +131,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Lendude hilinemised', 'settings.notifyFlightLanded': 'Maandumised', 'settings.notifyTripReminder': 'Reisimeeldetuletused', + 'settings.notifyMailBookingAdded': 'Kirjadest lisatud broneeringud', + 'settings.notifyMailBookingPending': 'Ülevaatamist ootavad edastatud broneeringud', + 'settings.notifyMailIngestPaused': 'Kirjaimport peatatud', 'settings.notifyTodoDue': 'Ülesande tähtaeg läheneb', 'settings.notifyVacayInvite': 'Vacay ühendamiskutsed', 'settings.notifyVacayShare': 'Vacay kalendri jagamised', diff --git a/shared/src/i18n/fr/index.ts b/shared/src/i18n/fr/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/fr/index.ts +++ b/shared/src/i18n/fr/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/fr/mailAdmin.ts b/shared/src/i18n/fr/mailAdmin.ts new file mode 100644 index 000000000..3846a307c --- /dev/null +++ b/shared/src/i18n/fr/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Import des e-mails de réservation', + 'mailAdmin.hint': 'Les confirmations de réservation transférées deviennent des réservations dans le bon voyage.', + 'mailAdmin.enabled': 'Importer les e-mails de réservation', + 'mailAdmin.enabledHint': + 'Donne à chaque utilisateur de cette installation une adresse de transfert. Désactivé, aucune adresse ne reçoit d’e-mails.', + 'mailAdmin.transport.label': 'Mode de réception des e-mails', + 'mailAdmin.transport.off': 'Import de fichiers uniquement', + 'mailAdmin.transport.imap': 'Boîte aux lettres IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'E-mails traités par IA par personne et par jour', + 'mailAdmin.aiDailyHint': 'Au-delà, seules les données de réservation lisibles par machine sont lues.', + 'mailAdmin.template': 'Modèle d’adresse', + 'mailAdmin.templateHint': 'L’adresse de transfert, avec {token} à l’endroit où va le code de chaque personne.', + 'mailAdmin.imap.host': 'Serveur', + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Chiffrement', + 'mailAdmin.imap.user': 'Utilisateur', + 'mailAdmin.imap.pass': 'Mot de passe', + 'mailAdmin.imap.mailbox': 'Dossier', + 'mailAdmin.imap.mailboxHint': 'Les e-mails lus sont déplacés vers TREK/processed.', + 'mailAdmin.webhook.secret': 'Secret de signature', + 'mailAdmin.webhook.secretHint': + 'Au moins 32 caractères ; l’expéditeur signe chaque e-mail avec. Vide, le webhook est désactivé.', + 'mailAdmin.inboundUrl': 'Adresse du webhook', + 'mailAdmin.inboundUrlHint': 'Votre service de messagerie y envoie chaque e-mail, brut et signé.', + 'mailAdmin.copyUrl': 'Copier l’adresse', + 'mailAdmin.envHint': 'Les variables d’environnement (MAIL_INGEST_*) l’emportent sur ces champs.', + 'mailAdmin.loadError': 'Les paramètres d’import des e-mails n’ont pas pu être chargés.', + 'mailAdmin.problem.secretShort': 'Le secret doit comporter au moins 32 caractères.', + 'mailAdmin.problem.portInvalid': 'Saisissez un port entre 1 et 65535.', + 'mailAdmin.problem.templateToken': 'L’adresse doit contenir {token} pour le code de chaque personne.', + 'mailAdmin.problem.dailyInvalid': 'Saisissez un nombre entier de 0 à 1000.', + 'mailAdmin.status.title': 'État', + 'mailAdmin.status.lastRead': 'Dernière lecture {time}', + 'mailAdmin.status.never': 'jamais', + 'mailAdmin.status.mails': 'E-mails, 24 h', + 'mailAdmin.status.added': 'Ajoutés automatiquement', + 'mailAdmin.status.waiting': 'En attente', + 'mailAdmin.status.failed': 'Échoués', + 'mailAdmin.test.run': 'Tester la connexion', + 'mailAdmin.test.ok': 'Connecté', + 'mailAdmin.test.failed': 'Pas de connexion', +}; +export default mailAdmin; diff --git a/shared/src/i18n/fr/mailInbox.ts b/shared/src/i18n/fr/mailInbox.ts new file mode 100644 index 000000000..f0c18d175 --- /dev/null +++ b/shared/src/i18n/fr/mailInbox.ts @@ -0,0 +1,100 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Réservations en attente', + 'mailInbox.pendingCount': '{count} réservations vous attendent', + 'mailInbox.pendingCount.one': '{count} réservation vous attend', + 'mailInbox.empty': 'Rien en attente', + 'mailInbox.emptyHint': + 'Les réservations que TREK n’a pas pu ajouter seul apparaissent ici, avec ce que vous pouvez faire pour chacune.', + 'mailInbox.untitled': 'E-mail de réservation', + 'mailInbox.showMail': 'Afficher l’e-mail', + 'mailInbox.backToList': 'Retour à la liste', + 'mailInbox.loadMore': 'Afficher plus', + 'mailInbox.pickTrip': 'Quel voyage ?', + 'mailInbox.added': 'Réservation ajoutée', + 'mailInbox.filed': 'Enregistré dans les fichiers du voyage', + 'mailInbox.discarded': 'Ignoré', + 'mailInbox.actionError': 'Ça n’a pas fonctionné. Réessayez dans un instant.', + 'mailInbox.previewError': 'Impossible d’afficher l’e-mail.', + 'mailInbox.action.add': 'Ajouter', + 'mailInbox.action.choose': 'Ajouter à un voyage', + 'mailInbox.action.file': 'Enregistrer comme document', + 'mailInbox.action.manual': 'Remplir soi-même', + 'mailInbox.action.discard': 'Ignorer', + 'mailInbox.reason.forwardingConfirmation': + 'Votre fournisseur de messagerie demande de confirmer la redirection. Saisissez ce code chez lui.', + 'mailInbox.reason.newsletter': 'Une newsletter, pas une réservation', + 'mailInbox.reason.autoReply': 'Une réponse automatique', + 'mailInbox.reason.emptyMail': 'L’e-mail ne contient ni texte ni pièce jointe à lire', + 'mailInbox.reason.mailUnreadable': 'L’e-mail n’a pas pu être lu', + 'mailInbox.reason.notABooking': 'Aucune réservation trouvée dans cet e-mail', + 'mailInbox.reason.tooLarge': 'L’e-mail est trop volumineux pour être lu', + 'mailInbox.reason.addressInvalid': 'Reçu sur une adresse qui ne fonctionne plus', + 'mailInbox.reason.addressDailyLimit': 'Votre adresse a déjà reçu tous ses e-mails du jour', + 'mailInbox.reason.processingFailed': 'La lecture a échoué plusieurs fois', + 'mailInbox.reason.aiQuotaExhausted': 'Le quota d’IA de cette période est épuisé', + 'mailInbox.reason.aiDisabled': 'Lu sans IA, qui n’a trouvé aucune réservation', + 'mailInbox.reason.aiUserCap': 'Votre limite quotidienne d’e-mails lus par IA est atteinte', + 'mailInbox.reason.aiProviderError': 'L’IA n’a pas pu le lire pour l’instant', + 'mailInbox.reason.noTripInRange': 'Aucun voyage ne couvre ces dates', + 'mailInbox.reason.tripAmbiguous': 'Plus d’un voyage peut correspondre', + 'mailInbox.reason.tripWithoutDates': 'Le voyage n’a pas de dates à comparer', + 'mailInbox.reason.noEditPermission': 'Vous ne pouvez pas ajouter de réservations à ce voyage', + 'mailInbox.reason.demoMode': 'La démo n’ajoute aucune réservation', + 'mailInbox.reason.outsideTripDates': 'Une date tombe en dehors du voyage', + 'mailInbox.reason.sourceMismatch': 'Les informations de l’e-mail se contredisent', + 'mailInbox.reason.implausible': 'Une valeur ne peut pas être exacte', + 'mailInbox.reason.notInText': 'Une valeur lue ne figure pas dans l’e-mail', + 'mailInbox.reason.imageOnly': 'Lu à partir d’une image seulement, il faut donc le vérifier', + 'mailInbox.reason.missingCriticalField': 'Le type, la date ou le trajet manque', + 'mailInbox.reason.missingConfirmation': 'Aucune référence de réservation trouvée', + 'mailInbox.reason.tooManyBookings': 'Plus de réservations dans un seul e-mail que la limite d’ajout automatique', + 'mailInbox.reason.changeOrCancellation': 'Une modification ou une annulation d’une réservation précédente', + 'mailInbox.reason.cancelled': 'La réservation est annulée', + 'mailInbox.reason.possibleDuplicate': 'Le voyage contient peut-être déjà cette réservation', + 'mailInbox.reason.changeAvailable': 'Le voyage contient cette réservation, et l’e-mail la modifie', + 'mailInbox.reason.conflict': 'Le voyage contient cette réservation avec d’autres informations', + 'mailInbox.reason.askFirst': 'Vous avez demandé à confirmer chaque réservation', + 'mailInbox.settings.title': 'E-mails de réservation', + 'mailInbox.settings.on': 'Activé', + 'mailInbox.settings.off': 'Désactivé', + 'mailInbox.settings.yourAddress': 'Votre adresse', + 'mailInbox.settings.copy': 'Copier l’adresse', + 'mailInbox.settings.copyError': 'Copie impossible', + 'mailInbox.settings.renew': 'Nouvelle adresse', + 'mailInbox.settings.renewConfirm': 'Touchez à nouveau : l’ancienne adresse cessera de fonctionner', + 'mailInbox.settings.renewed': 'Votre nouvelle adresse est prête', + 'mailInbox.settings.saveError': 'Enregistrement impossible', + 'mailInbox.settings.addressHint': + 'Transférez les confirmations de réservation à cette adresse et TREK les ajoute au bon voyage. Gardez-la pour vous, comme un mot de passe.', + 'mailInbox.settings.howTo': 'Transférer automatiquement', + 'mailInbox.settings.howToGmail': + 'Gmail : Paramètres, Transfert et POP/IMAP, Ajouter une adresse de transfert. Gmail envoie un code, qui apparaît sous Réservations en attente. Ajoutez ensuite un filtre pour les expéditeurs de réservations qui transfère à TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook : Paramètres, Courrier, Règles, ajoutez une règle qui transfère les e-mails de réservation. Certains comptes professionnels bloquent le transfert automatique ; transférez alors à la main.', + 'mailInbox.settings.howToManual': + 'À la main : transférez n’importe quel e-mail de réservation, pièces jointes comprises.', + 'mailInbox.settings.enabled': 'Importer les e-mails de réservation', + 'mailInbox.settings.enabledHint': 'Désactivé, les e-mails envoyés à votre adresse ne sont pas lus.', + 'mailInbox.settings.ai': 'Lire avec l’IA', + 'mailInbox.settings.aiHint': + 'Lit n’importe quel e-mail de réservation et trouve son voyage. L’e-mail ainsi que les dates et destinations de vos voyages sont transmis au fournisseur d’IA.', + 'mailInbox.settings.aiUnavailable': + 'Indisponible ici : seuls les e-mails contenant des données de réservation lisibles par machine sont lus.', + 'mailInbox.settings.autoAdd': 'Ajouter automatiquement', + 'mailInbox.settings.autoAddHint': + 'Une réservation dont TREK est sûr va directement dans son voyage, et vous pouvez l’annuler pendant 7 jours. Désactivé, chacune attend votre accord.', + 'mailInbox.settings.upload': 'Importer un fichier d’e-mail', + 'mailInbox.settings.uploaded': 'E-mail reçu, lecture en cours', + 'mailInbox.settings.uploadDuplicate': 'Cet e-mail a déjà été importé', + 'mailInbox.settings.uploadError': 'Le fichier n’a pas pu être lu comme un e-mail', + 'mailInbox.settings.privacy': + 'Les e-mails transférés sont conservés chiffrés 30 jours au maximum, puis supprimés. Merci de ne pas transférer de pièces d’identité ni de documents de santé.', + 'mailInbox.trip.title': 'Ou transférez par e-mail', + 'mailInbox.trip.hint': 'Un e-mail de réservation envoyé à l’adresse de ce voyage arrive ici sans aucune devinette.', + 'mailInbox.trip.create': 'Créer une adresse pour ce voyage', + 'mailInbox.fromMail': 'Depuis un e-mail', + 'mailInbox.fromMailHint': 'Ajouté depuis un e-mail de réservation transféré', +}; +export default mailInbox; diff --git a/shared/src/i18n/fr/notif.ts b/shared/src/i18n/fr/notif.ts index c7be461db..92b3e1c63 100644 --- a/shared/src/i18n/fr/notif.ts +++ b/shared/src/i18n/fr/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} a été ajouté à {trip}', 'notif.mail_booking_added.shared': '{actor} a ajouté {booking} à {trip} depuis un e-mail transféré', 'notif.mail_booking_pending.title': 'Réservations transférées à vérifier', - 'notif.mail_booking_pending.text': 'Réservations transférées en attente : {count}', + 'notif.mail_booking_pending.text': '{count} réservations transférées vous attendent', + 'notif.mail_booking_pending.text.one': '{count} réservation transférée vous attend', 'notif.mail_ingest_paused.title': 'Import des e-mails en pause', 'notif.mail_ingest_paused.text': 'Les e-mails de réservation transférés ne sont pas acceptés pour le moment. Vos adresses restent et fonctionneront de nouveau au retour de l’import.', diff --git a/shared/src/i18n/fr/reservations.ts b/shared/src/i18n/fr/reservations.ts index d55200e38..62636ead6 100644 --- a/shared/src/i18n/fr/reservations.ts +++ b/shared/src/i18n/fr/reservations.ts @@ -37,7 +37,10 @@ const reservations: TranslationStrings = { 'reservations.layover.layover': 'Escale', 'reservations.needsReview': 'Vérifier', 'reservations.needsReviewHint': - "L'aéroport n'a pas pu être identifié automatiquement — veuillez confirmer l'emplacement.", + 'Certaines informations ont été remplies automatiquement et n’ont pas pu être lues avec certitude. Merci de les vérifier.', + 'reservations.reviewConfirm': 'C’est correct', + 'reservations.reviewConfirmHint': 'Cliquez pour marquer comme vérifié.', + 'reservations.reviewConfirmed': 'Marqué comme vérifié', 'reservations.searchLocation': 'Rechercher une gare, un port, une adresse…', 'reservations.meta.trainNumber': 'N° de train', 'reservations.meta.platform': 'Quai', diff --git a/shared/src/i18n/fr/settings.ts b/shared/src/i18n/fr/settings.ts index be5ae4a95..3a8345632 100644 --- a/shared/src/i18n/fr/settings.ts +++ b/shared/src/i18n/fr/settings.ts @@ -118,6 +118,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Retards de vol', 'settings.notifyFlightLanded': 'Atterrissages', 'settings.notifyTripReminder': 'Rappels de voyage', + 'settings.notifyMailBookingAdded': 'Réservations ajoutées depuis des e-mails', + 'settings.notifyMailBookingPending': 'Réservations transférées à vérifier', + 'settings.notifyMailIngestPaused': 'Import des e-mails en pause', 'settings.notifyTodoDue': 'Tâche à échéance', 'settings.notifyVacayInvite': 'Invitations de fusion Vacay', 'settings.notifyVacayShare': 'Partages de calendrier Vacay', diff --git a/shared/src/i18n/gr/index.ts b/shared/src/i18n/gr/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/gr/index.ts +++ b/shared/src/i18n/gr/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/gr/mailAdmin.ts b/shared/src/i18n/gr/mailAdmin.ts new file mode 100644 index 000000000..30a68890b --- /dev/null +++ b/shared/src/i18n/gr/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Εισαγωγή email κρατήσεων', + 'mailAdmin.hint': 'Οι προωθημένες επιβεβαιώσεις κράτησης γίνονται κρατήσεις στο σωστό ταξίδι.', + 'mailAdmin.enabled': 'Εισαγωγή email κρατήσεων', + 'mailAdmin.enabledHint': + 'Δίνει σε όλους σε αυτή την εγκατάσταση μια διεύθυνση προώθησης. Όταν είναι ανενεργό, καμία διεύθυνση δεν δέχεται email.', + 'mailAdmin.transport.label': 'Πώς φτάνουν τα email', + 'mailAdmin.transport.off': 'Μόνο μεταφόρτωση', + 'mailAdmin.transport.imap': 'Γραμματοκιβώτιο IMAP', + 'mailAdmin.transport.webhook': 'Webhook', + 'mailAdmin.aiDaily': 'Email AI ανά άτομο και ημέρα', + 'mailAdmin.aiDailyHint': 'Πάνω από αυτό διαβάζονται μόνο δεδομένα κράτησης αναγνώσιμα από μηχανή.', + 'mailAdmin.template': 'Πρότυπο διεύθυνσης', + 'mailAdmin.templateHint': 'Η διεύθυνση προώθησης, με {token} εκεί που μπαίνει ο κωδικός κάθε ατόμου.', + 'mailAdmin.imap.host': 'Διακομιστής', + 'mailAdmin.imap.port': 'Θύρα', + 'mailAdmin.imap.tls': 'Κρυπτογράφηση', + 'mailAdmin.imap.user': 'Χρήστης', + 'mailAdmin.imap.pass': 'Κωδικός πρόσβασης', + 'mailAdmin.imap.mailbox': 'Φάκελος', + 'mailAdmin.imap.mailboxHint': 'Τα email που διαβάστηκαν μεταφέρονται στο TREK/processed.', + 'mailAdmin.webhook.secret': 'Μυστικό υπογραφής', + 'mailAdmin.webhook.secretHint': + 'Τουλάχιστον 32 χαρακτήρες· ο αποστολέας υπογράφει κάθε email με αυτό. Αν μείνει κενό, το webhook απενεργοποιείται.', + 'mailAdmin.inboundUrl': 'Διεύθυνση webhook', + 'mailAdmin.inboundUrlHint': 'Η υπηρεσία email σου στέλνει εδώ κάθε email, ανεπεξέργαστο και υπογεγραμμένο.', + 'mailAdmin.copyUrl': 'Αντιγραφή διεύθυνσης', + 'mailAdmin.envHint': 'Οι μεταβλητές περιβάλλοντος (MAIL_INGEST_*) υπερισχύουν αυτών των πεδίων.', + 'mailAdmin.loadError': 'Οι ρυθμίσεις εισαγωγής email δεν μπόρεσαν να φορτωθούν.', + 'mailAdmin.problem.secretShort': 'Το μυστικό χρειάζεται τουλάχιστον 32 χαρακτήρες.', + 'mailAdmin.problem.portInvalid': 'Εισήγαγε θύρα από 1 έως 65535.', + 'mailAdmin.problem.templateToken': 'Η διεύθυνση χρειάζεται {token} για τον κωδικό κάθε ατόμου.', + 'mailAdmin.problem.dailyInvalid': 'Εισήγαγε έναν ακέραιο αριθμό από 0 έως 1000.', + 'mailAdmin.status.title': 'Κατάσταση', + 'mailAdmin.status.lastRead': 'Τελευταία ανάγνωση {time}', + 'mailAdmin.status.never': 'ποτέ', + 'mailAdmin.status.mails': 'Email, 24 ώ', + 'mailAdmin.status.added': 'Προστέθηκαν αυτόματα', + 'mailAdmin.status.waiting': 'Σε αναμονή', + 'mailAdmin.status.failed': 'Απέτυχαν', + 'mailAdmin.test.run': 'Δοκιμή σύνδεσης', + 'mailAdmin.test.ok': 'Συνδέθηκε', + 'mailAdmin.test.failed': 'Χωρίς σύνδεση', +}; +export default mailAdmin; diff --git a/shared/src/i18n/gr/mailInbox.ts b/shared/src/i18n/gr/mailInbox.ts new file mode 100644 index 000000000..0656f24d6 --- /dev/null +++ b/shared/src/i18n/gr/mailInbox.ts @@ -0,0 +1,100 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Κρατήσεις σε αναμονή', + 'mailInbox.pendingCount': '{count} κρατήσεις σε περιμένουν', + 'mailInbox.pendingCount.one': '{count} κράτηση σε περιμένει', + 'mailInbox.empty': 'Τίποτα σε αναμονή', + 'mailInbox.emptyHint': + 'Οι κρατήσεις που το TREK δεν μπόρεσε να προσθέσει μόνο του εμφανίζονται εδώ, μαζί με το τι μπορείς να κάνεις για την καθεμία.', + 'mailInbox.untitled': 'Email κράτησης', + 'mailInbox.showMail': 'Εμφάνιση email', + 'mailInbox.backToList': 'Πίσω στη λίστα', + 'mailInbox.loadMore': 'Εμφάνιση περισσότερων', + 'mailInbox.pickTrip': 'Ποιο ταξίδι;', + 'mailInbox.added': 'Η κράτηση προστέθηκε', + 'mailInbox.filed': 'Αποθηκεύτηκε στα αρχεία του ταξιδιού', + 'mailInbox.discarded': 'Απορρίφθηκε', + 'mailInbox.actionError': 'Δεν λειτούργησε. Δοκίμασε ξανά σε λίγο.', + 'mailInbox.previewError': 'Το email δεν μπόρεσε να εμφανιστεί.', + 'mailInbox.action.add': 'Προσθήκη', + 'mailInbox.action.choose': 'Προσθήκη σε ταξίδι', + 'mailInbox.action.file': 'Αποθήκευση ως έγγραφο', + 'mailInbox.action.manual': 'Συμπλήρωση μόνος', + 'mailInbox.action.discard': 'Απόρριψη', + 'mailInbox.reason.forwardingConfirmation': + 'Ο πάροχος email σου ζητά επιβεβαίωση της προώθησης. Πληκτρολόγησε αυτόν τον κωδικό εκεί.', + 'mailInbox.reason.newsletter': 'Ενημερωτικό δελτίο, όχι κράτηση', + 'mailInbox.reason.autoReply': 'Αυτόματη απάντηση', + 'mailInbox.reason.emptyMail': 'Το email δεν έχει κείμενο ή συνημμένο για ανάγνωση', + 'mailInbox.reason.mailUnreadable': 'Το email δεν μπόρεσε να διαβαστεί', + 'mailInbox.reason.notABooking': 'Δεν βρέθηκε κράτηση σε αυτό το email', + 'mailInbox.reason.tooLarge': 'Το email είναι πολύ μεγάλο για να διαβαστεί', + 'mailInbox.reason.addressInvalid': 'Ήρθε σε διεύθυνση που δεν λειτουργεί πια', + 'mailInbox.reason.addressDailyLimit': 'Η διεύθυνσή σου δέχτηκε όλα τα email της σημερινής ημέρας', + 'mailInbox.reason.processingFailed': 'Η ανάγνωση απέτυχε αρκετές φορές', + 'mailInbox.reason.aiQuotaExhausted': 'Το όριο AI αυτής της περιόδου έχει εξαντληθεί', + 'mailInbox.reason.aiDisabled': 'Διαβάστηκε χωρίς AI, που δεν βρήκε κράτηση', + 'mailInbox.reason.aiUserCap': 'Έφτασες το ημερήσιο όριο email που διαβάζονται με AI', + 'mailInbox.reason.aiProviderError': 'Το AI δεν μπόρεσε να το διαβάσει αυτή τη στιγμή', + 'mailInbox.reason.noTripInRange': 'Κανένα ταξίδι δεν καλύπτει αυτές τις ημερομηνίες', + 'mailInbox.reason.tripAmbiguous': 'Θα μπορούσε να εννοείται πάνω από ένα ταξίδι', + 'mailInbox.reason.tripWithoutDates': 'Το ταξίδι δεν έχει ημερομηνίες για έλεγχο', + 'mailInbox.reason.noEditPermission': 'Δεν επιτρέπεται να προσθέσεις κρατήσεις σε αυτό το ταξίδι', + 'mailInbox.reason.demoMode': 'Το demo δεν προσθέτει κρατήσεις', + 'mailInbox.reason.outsideTripDates': 'Μια ημερομηνία είναι εκτός του ταξιδιού', + 'mailInbox.reason.sourceMismatch': 'Τα στοιχεία του email δεν συμφωνούν μεταξύ τους', + 'mailInbox.reason.implausible': 'Μια τιμή δεν μπορεί να είναι σωστή', + 'mailInbox.reason.notInText': 'Μια τιμή που διαβάστηκε δεν υπάρχει στο email', + 'mailInbox.reason.imageOnly': 'Διαβάστηκε μόνο από εικόνα, οπότε χρειάζεται να το ελέγξεις', + 'mailInbox.reason.missingCriticalField': 'Λείπει ο τύπος, η ημερομηνία ή η διαδρομή', + 'mailInbox.reason.missingConfirmation': 'Δεν βρέθηκε κωδικός κράτησης', + 'mailInbox.reason.tooManyBookings': 'Περισσότερες κρατήσεις σε ένα email από όσες προστίθενται αυτόματα', + 'mailInbox.reason.changeOrCancellation': 'Αλλαγή ή ακύρωση προηγούμενης κράτησης', + 'mailInbox.reason.cancelled': 'Η κράτηση έχει ακυρωθεί', + 'mailInbox.reason.possibleDuplicate': 'Το ταξίδι ίσως έχει ήδη αυτή την κράτηση', + 'mailInbox.reason.changeAvailable': 'Το ταξίδι έχει αυτή την κράτηση και το email την αλλάζει', + 'mailInbox.reason.conflict': 'Το ταξίδι έχει αυτή την κράτηση με άλλα στοιχεία', + 'mailInbox.reason.askFirst': 'Ζήτησες επιβεβαίωση για κάθε κράτηση', + 'mailInbox.settings.title': 'Email κρατήσεων', + 'mailInbox.settings.on': 'Ενεργό', + 'mailInbox.settings.off': 'Ανενεργό', + 'mailInbox.settings.yourAddress': 'Η διεύθυνσή σου', + 'mailInbox.settings.copy': 'Αντιγραφή διεύθυνσης', + 'mailInbox.settings.copyError': 'Δεν ήταν δυνατή η αντιγραφή', + 'mailInbox.settings.renew': 'Νέα διεύθυνση', + 'mailInbox.settings.renewConfirm': 'Πάτα ξανά: η παλιά διεύθυνση θα σταματήσει να λειτουργεί', + 'mailInbox.settings.renewed': 'Η νέα διεύθυνσή σου είναι έτοιμη', + 'mailInbox.settings.saveError': 'Δεν ήταν δυνατή η αποθήκευση', + 'mailInbox.settings.addressHint': + 'Προώθησε τις επιβεβαιώσεις κράτησης σε αυτή τη διεύθυνση και το TREK τις προσθέτει στο σωστό ταξίδι. Κράτα την για τον εαυτό σου σαν κωδικό πρόσβασης.', + 'mailInbox.settings.howTo': 'Αυτόματη προώθηση', + 'mailInbox.settings.howToGmail': + 'Gmail: Ρυθμίσεις, Προώθηση και POP/IMAP, Προσθήκη διεύθυνσης προώθησης. Το Gmail στέλνει έναν κωδικό, που εμφανίζεται στις Κρατήσεις σε αναμονή. Έπειτα πρόσθεσε ένα φίλτρο για αποστολείς κρατήσεων που προωθεί στο TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Ρυθμίσεις, Αλληλογραφία, Κανόνες, πρόσθεσε έναν κανόνα που προωθεί τα email κρατήσεων. Ορισμένοι εργασιακοί λογαριασμοί μπλοκάρουν την αυτόματη προώθηση, εκεί προώθησέ τα χειροκίνητα.', + 'mailInbox.settings.howToManual': 'Χειροκίνητα: προώθησε οποιοδήποτε email κράτησης, μαζί με τα συνημμένα.', + 'mailInbox.settings.enabled': 'Εισαγωγή email κρατήσεων', + 'mailInbox.settings.enabledHint': 'Όταν είναι ανενεργό, τα email προς τη διεύθυνσή σου δεν διαβάζονται.', + 'mailInbox.settings.ai': 'Ανάγνωση με AI', + 'mailInbox.settings.aiHint': + 'Διαβάζει κάθε email κράτησης και βρίσκει το ταξίδι του. Το email και οι ημερομηνίες και οι προορισμοί των ταξιδιών σου πηγαίνουν στον πάροχο AI.', + 'mailInbox.settings.aiUnavailable': + 'Δεν είναι διαθέσιμο εδώ: διαβάζονται μόνο email με δεδομένα κράτησης αναγνώσιμα από μηχανή.', + 'mailInbox.settings.autoAdd': 'Αυτόματη προσθήκη', + 'mailInbox.settings.autoAddHint': + 'Μια κράτηση για την οποία το TREK είναι σίγουρο μπαίνει κατευθείαν στο ταξίδι της και μπορείς να την αναιρέσεις για 7 ημέρες. Όταν είναι ανενεργό, κάθε κράτηση περιμένει το OK σου.', + 'mailInbox.settings.upload': 'Μεταφόρτωση αρχείου email', + 'mailInbox.settings.uploaded': 'Το email ελήφθη και διαβάζεται', + 'mailInbox.settings.uploadDuplicate': 'Αυτό το email υπάρχει ήδη', + 'mailInbox.settings.uploadError': 'Το αρχείο δεν μπόρεσε να διαβαστεί ως email', + 'mailInbox.settings.privacy': + 'Τα προωθημένα email φυλάσσονται κρυπτογραφημένα για το πολύ 30 ημέρες και μετά διαγράφονται. Μην προωθείς έγγραφα ταυτότητας ή υγείας.', + 'mailInbox.trip.title': 'Ή προώθηση με email', + 'mailInbox.trip.hint': + 'Ένα email κράτησης που στέλνεται στη διεύθυνση αυτού του ταξιδιού καταλήγει εδώ χωρίς εικασίες.', + 'mailInbox.trip.create': 'Δημιουργία διεύθυνσης για αυτό το ταξίδι', + 'mailInbox.fromMail': 'Από email', + 'mailInbox.fromMailHint': 'Προστέθηκε από προωθημένο email κράτησης', +}; +export default mailInbox; diff --git a/shared/src/i18n/gr/notif.ts b/shared/src/i18n/gr/notif.ts index eb9723296..eff3b06f6 100644 --- a/shared/src/i18n/gr/notif.ts +++ b/shared/src/i18n/gr/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': 'Η κράτηση {booking} προστέθηκε στο {trip}', 'notif.mail_booking_added.shared': 'Ο/Η {actor} πρόσθεσε την κράτηση {booking} στο {trip} από προωθημένο email', 'notif.mail_booking_pending.title': 'Προωθημένες κρατήσεις για έλεγχο', - 'notif.mail_booking_pending.text': 'Προωθημένες κρατήσεις σε αναμονή: {count}', + 'notif.mail_booking_pending.text': '{count} προωθημένες κρατήσεις σε περιμένουν', + 'notif.mail_booking_pending.text.one': '{count} προωθημένη κράτηση σε περιμένει', 'notif.mail_ingest_paused.title': 'Η εισαγωγή email σε παύση', 'notif.mail_ingest_paused.text': 'Τα προωθημένα email κρατήσεων δεν γίνονται δεκτά προς το παρόν. Οι διευθύνσεις σου μένουν και θα λειτουργήσουν ξανά όταν επιστρέψει η εισαγωγή.', diff --git a/shared/src/i18n/gr/reservations.ts b/shared/src/i18n/gr/reservations.ts index 2c19db879..f8837f98b 100644 --- a/shared/src/i18n/gr/reservations.ts +++ b/shared/src/i18n/gr/reservations.ts @@ -37,7 +37,10 @@ const reservations: TranslationStrings = { 'reservations.layover.layover': 'Ενδιάμεση στάση', 'reservations.needsReview': 'Έλεγχος', 'reservations.needsReviewHint': - 'Δεν ήταν δυνατή η αυτόματη αντιστοίχιση του αεροδρομίου — παρακαλώ επιβεβαιώστε την τοποθεσία.', + 'Ορισμένα στοιχεία συμπληρώθηκαν αυτόματα και δεν μπόρεσαν να διαβαστούν με βεβαιότητα. Έλεγξέ τα.', + 'reservations.reviewConfirm': 'Φαίνεται σωστό', + 'reservations.reviewConfirmHint': 'Κάνε κλικ για να σημειωθεί ως ελεγμένο.', + 'reservations.reviewConfirmed': 'Σημειώθηκε ως ελεγμένο', 'reservations.searchLocation': 'Αναζήτηση σταθμού, λιμανιού, διεύθυνσης…', 'reservations.meta.trainNumber': 'Αρ. Τρένου', 'reservations.meta.platform': 'Αποβάθρα', diff --git a/shared/src/i18n/gr/settings.ts b/shared/src/i18n/gr/settings.ts index 6cb78ea2e..902c5585c 100644 --- a/shared/src/i18n/gr/settings.ts +++ b/shared/src/i18n/gr/settings.ts @@ -123,6 +123,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Καθυστερήσεις πτήσεων', 'settings.notifyFlightLanded': 'Προσγειώσεις πτήσεων', 'settings.notifyTripReminder': 'Υπενθυμίσεις ταξιδιού', + 'settings.notifyMailBookingAdded': 'Κρατήσεις που προστέθηκαν από email', + 'settings.notifyMailBookingPending': 'Προωθημένες κρατήσεις προς έλεγχο', + 'settings.notifyMailIngestPaused': 'Η εισαγωγή email σταμάτησε', 'settings.notifyTodoDue': 'Εκκρεμότητα λήγει σύντομα', 'settings.notifyVacayInvite': 'Προσκλήσεις Vacay fusion', 'settings.notifyVacayShare': 'Κοινοποιήσεις ημερολογίου Vacay', diff --git a/shared/src/i18n/he/index.ts b/shared/src/i18n/he/index.ts index a58e37e01..f44e7c981 100644 --- a/shared/src/i18n/he/index.ts +++ b/shared/src/i18n/he/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/he/mailAdmin.ts b/shared/src/i18n/he/mailAdmin.ts new file mode 100644 index 000000000..70d042265 --- /dev/null +++ b/shared/src/i18n/he/mailAdmin.ts @@ -0,0 +1,45 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'ייבוא דוא״ל הזמנות', + 'mailAdmin.hint': 'אישורי הזמנה שהועברו הופכים להזמנות בטיול המתאים.', + 'mailAdmin.enabled': 'ייבוא דוא״ל הזמנות', + 'mailAdmin.enabledHint': 'נותן לכל משתמש בהתקנה הזו כתובת להעברה. כשהאפשרות כבויה, אף כתובת לא מקבלת דוא״ל.', + 'mailAdmin.transport.label': 'איך הדוא״ל מגיע', + 'mailAdmin.transport.off': 'העלאה בלבד', + 'mailAdmin.transport.imap': 'תיבת דואר IMAP', + 'mailAdmin.transport.webhook': 'וובהוק', + 'mailAdmin.aiDaily': 'דוא״ל בבינה מלאכותית לאדם ליום', + 'mailAdmin.aiDailyHint': 'מעבר לכך, נקראים רק נתוני הזמנה שניתנים לקריאה אוטומטית.', + 'mailAdmin.template': 'תבנית כתובת', + 'mailAdmin.templateHint': 'כתובת ההעברה, עם {token} במקום שבו מופיע הקוד של כל אדם.', + 'mailAdmin.imap.host': 'שרת', + 'mailAdmin.imap.port': 'פורט', + 'mailAdmin.imap.tls': 'הצפנה', + 'mailAdmin.imap.user': 'משתמש', + 'mailAdmin.imap.pass': 'סיסמה', + 'mailAdmin.imap.mailbox': 'תיקייה', + 'mailAdmin.imap.mailboxHint': 'דוא״ל שנקרא עובר אל TREK/processed.', + 'mailAdmin.webhook.secret': 'סוד חתימה', + 'mailAdmin.webhook.secretHint': 'לפחות 32 תווים; השולח חותם בו כל דוא״ל. שדה ריק מכבה את הוובהוק.', + 'mailAdmin.inboundUrl': 'כתובת וובהוק', + 'mailAdmin.inboundUrlHint': 'שירות הדוא״ל שלכם שולח לכאן כל דוא״ל, גולמי וחתום.', + 'mailAdmin.copyUrl': 'העתקת הכתובת', + 'mailAdmin.envHint': 'משתני סביבה (MAIL_INGEST_*) גוברים על השדות האלה.', + 'mailAdmin.loadError': 'לא ניתן היה לטעון את הגדרות ייבוא הדוא״ל.', + 'mailAdmin.problem.secretShort': 'הסוד חייב להכיל לפחות 32 תווים.', + 'mailAdmin.problem.portInvalid': 'הזינו פורט בין 1 ל-65535.', + 'mailAdmin.problem.templateToken': 'הכתובת צריכה {token} במקום הקוד של כל אדם.', + 'mailAdmin.problem.dailyInvalid': 'הזינו מספר שלם בין 0 ל-1000.', + 'mailAdmin.status.title': 'סטטוס', + 'mailAdmin.status.lastRead': 'נקרא לאחרונה {time}', + 'mailAdmin.status.never': 'אף פעם', + 'mailAdmin.status.mails': 'דוא״ל, 24 שעות', + 'mailAdmin.status.added': 'נוספו אוטומטית', + 'mailAdmin.status.waiting': 'ממתינות', + 'mailAdmin.status.failed': 'נכשלו', + 'mailAdmin.test.run': 'בדיקת חיבור', + 'mailAdmin.test.ok': 'מחובר', + 'mailAdmin.test.failed': 'אין חיבור', +}; +export default mailAdmin; diff --git a/shared/src/i18n/he/mailInbox.ts b/shared/src/i18n/he/mailInbox.ts new file mode 100644 index 000000000..4582add9a --- /dev/null +++ b/shared/src/i18n/he/mailInbox.ts @@ -0,0 +1,97 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'הזמנות ממתינות', + 'mailInbox.pendingCount': '{count} הזמנות ממתינות לכם', + 'mailInbox.pendingCount.one': '{count} הזמנה ממתינה לכם', + 'mailInbox.pendingCount.two': '{count} הזמנות ממתינות לכם', + 'mailInbox.empty': 'אין דבר ממתין', + 'mailInbox.emptyHint': 'הזמנות ש-TREK לא הצליח להוסיף בעצמו מופיעות כאן, יחד עם האפשרויות לטפל בכל אחת.', + 'mailInbox.untitled': 'דוא״ל הזמנה', + 'mailInbox.showMail': 'הצגת הדוא״ל', + 'mailInbox.backToList': 'חזרה לרשימה', + 'mailInbox.loadMore': 'הצגת עוד', + 'mailInbox.pickTrip': 'לאיזה טיול?', + 'mailInbox.added': 'ההזמנה נוספה', + 'mailInbox.filed': 'נשמר בקבצי הטיול', + 'mailInbox.discarded': 'נמחק', + 'mailInbox.actionError': 'הפעולה לא הצליחה. נסו שוב בעוד רגע.', + 'mailInbox.previewError': 'לא ניתן היה להציג את הדוא״ל.', + 'mailInbox.action.add': 'הוספה', + 'mailInbox.action.choose': 'הוספה לטיול', + 'mailInbox.action.file': 'שמירה כמסמך', + 'mailInbox.action.manual': 'מילוי ידני', + 'mailInbox.action.discard': 'מחיקה', + 'mailInbox.reason.forwardingConfirmation': 'ספק הדוא״ל שלכם מבקש לאשר את ההעברה. הזינו שם את הקוד הזה.', + 'mailInbox.reason.newsletter': 'ניוזלטר, לא הזמנה', + 'mailInbox.reason.autoReply': 'תשובה אוטומטית', + 'mailInbox.reason.emptyMail': 'בדוא״ל אין טקסט או קובץ מצורף לקריאה', + 'mailInbox.reason.mailUnreadable': 'לא ניתן היה לקרוא את הדוא״ל', + 'mailInbox.reason.notABooking': 'לא נמצאה הזמנה בדוא״ל הזה', + 'mailInbox.reason.tooLarge': 'הדוא״ל גדול מדי לקריאה', + 'mailInbox.reason.addressInvalid': 'הוא הגיע לכתובת שכבר אינה פעילה', + 'mailInbox.reason.addressDailyLimit': 'הכתובת שלכם קיבלה היום את כל הדוא״ל שהיא יכולה', + 'mailInbox.reason.processingFailed': 'הקריאה נכשלה כמה פעמים', + 'mailInbox.reason.aiQuotaExhausted': 'מכסת הבינה המלאכותית של התקופה נוצלה', + 'mailInbox.reason.aiDisabled': 'נקרא ללא בינה מלאכותית, ולא נמצאה הזמנה', + 'mailInbox.reason.aiUserCap': 'הגעתם למגבלה היומית של דוא״ל הנקרא בבינה מלאכותית', + 'mailInbox.reason.aiProviderError': 'הבינה המלאכותית לא הצליחה לקרוא אותו כרגע', + 'mailInbox.reason.noTripInRange': 'אין טיול שמכסה את התאריכים האלה', + 'mailInbox.reason.tripAmbiguous': 'ייתכן שהכוונה ליותר מטיול אחד', + 'mailInbox.reason.tripWithoutDates': 'לטיול אין תאריכים להשוואה', + 'mailInbox.reason.noEditPermission': 'אין לכם הרשאה להוסיף הזמנות לטיול הזה', + 'mailInbox.reason.demoMode': 'גרסת ההדגמה אינה מוסיפה הזמנות', + 'mailInbox.reason.outsideTripDates': 'תאריך אחד חורג מתאריכי הטיול', + 'mailInbox.reason.sourceMismatch': 'פרטי הדוא״ל סותרים זה את זה', + 'mailInbox.reason.implausible': 'ערך אחד לא יכול להיות נכון', + 'mailInbox.reason.notInText': 'ערך שנקרא לא מופיע בדוא״ל', + 'mailInbox.reason.imageOnly': 'נקרא מתמונה בלבד, ולכן דרוש מבט שלכם', + 'mailInbox.reason.missingCriticalField': 'חסר סוג, תאריך או מסלול', + 'mailInbox.reason.missingConfirmation': 'לא נמצא מספר הזמנה', + 'mailInbox.reason.tooManyBookings': 'יותר הזמנות בדוא״ל אחד ממה שנוספות אוטומטית', + 'mailInbox.reason.changeOrCancellation': 'שינוי או ביטול של הזמנה קודמת', + 'mailInbox.reason.cancelled': 'ההזמנה בוטלה', + 'mailInbox.reason.possibleDuplicate': 'ייתכן שההזמנה כבר קיימת בטיול', + 'mailInbox.reason.changeAvailable': 'ההזמנה קיימת בטיול, והדוא״ל משנה אותה', + 'mailInbox.reason.conflict': 'ההזמנה קיימת בטיול עם פרטים אחרים', + 'mailInbox.reason.askFirst': 'ביקשתם לאשר כל הזמנה', + 'mailInbox.settings.title': 'דוא״ל הזמנות', + 'mailInbox.settings.on': 'פעיל', + 'mailInbox.settings.off': 'כבוי', + 'mailInbox.settings.yourAddress': 'הכתובת שלכם', + 'mailInbox.settings.copy': 'העתקת הכתובת', + 'mailInbox.settings.copyError': 'ההעתקה נכשלה', + 'mailInbox.settings.renew': 'כתובת חדשה', + 'mailInbox.settings.renewConfirm': 'הקישו שוב: הכתובת הישנה תפסיק לעבוד', + 'mailInbox.settings.renewed': 'הכתובת החדשה שלכם מוכנה', + 'mailInbox.settings.saveError': 'השמירה נכשלה', + 'mailInbox.settings.addressHint': + 'העבירו אישורי הזמנה לכתובת הזו ו-TREK יוסיף אותם לטיול המתאים. שמרו אותה לעצמכם כמו סיסמה.', + 'mailInbox.settings.howTo': 'העברה אוטומטית', + 'mailInbox.settings.howToGmail': + 'ג׳ימייל: הגדרות, העברה ו-POP/IMAP, הוספת כתובת להעברה. ג׳ימייל שולח קוד שמופיע תחת הזמנות ממתינות. לאחר מכן הוסיפו מסנן לשולחי הזמנות שמעביר אל TREK.', + 'mailInbox.settings.howToOutlook': + 'אאוטלוק: הגדרות, דואר, כללים, הוסיפו כלל שמעביר דוא״ל של הזמנות. חלק מחשבונות העבודה חוסמים העברה אוטומטית; שם העבירו ידנית.', + 'mailInbox.settings.howToManual': 'ידנית: העבירו כל דוא״ל של הזמנה, כולל הקבצים המצורפים.', + 'mailInbox.settings.enabled': 'ייבוא דוא״ל הזמנות', + 'mailInbox.settings.enabledHint': 'כשהאפשרות כבויה, דוא״ל שנשלח לכתובת שלכם לא נקרא.', + 'mailInbox.settings.ai': 'קריאה בבינה מלאכותית', + 'mailInbox.settings.aiHint': + 'קוראת כל דוא״ל של הזמנה ומוצאת את הטיול שלו. הדוא״ל ותאריכי הטיולים והיעדים שלכם נשלחים לספק הבינה המלאכותית.', + 'mailInbox.settings.aiUnavailable': 'לא זמין כאן: נקרא רק דוא״ל עם נתוני הזמנה שניתנים לקריאה אוטומטית.', + 'mailInbox.settings.autoAdd': 'הוספה אוטומטית', + 'mailInbox.settings.autoAddHint': + 'הזמנה ש-TREK בטוח בה נכנסת ישירות לטיול שלה, ואפשר לבטל אותה במשך 7 ימים. כשהאפשרות כבויה, כל הזמנה ממתינה לאישור שלכם.', + 'mailInbox.settings.upload': 'העלאת קובץ דוא״ל', + 'mailInbox.settings.uploaded': 'הדוא״ל התקבל ונקרא כעת', + 'mailInbox.settings.uploadDuplicate': 'הדוא״ל הזה כבר קיים', + 'mailInbox.settings.uploadError': 'לא ניתן היה לקרוא את הקובץ כדוא״ל', + 'mailInbox.settings.privacy': + 'דוא״ל שהועבר נשמר מוצפן עד 30 יום ואז נמחק. אנא אל תעבירו מסמכי זהות או מסמכים רפואיים.', + 'mailInbox.trip.title': 'או העברה בדוא״ל', + 'mailInbox.trip.hint': 'דוא״ל הזמנה שנשלח לכתובת של הטיול הזה מגיע לכאן בלי ניחושים.', + 'mailInbox.trip.create': 'יצירת כתובת לטיול הזה', + 'mailInbox.fromMail': 'מדוא״ל', + 'mailInbox.fromMailHint': 'נוסף מדוא״ל הזמנה שהועבר', +}; +export default mailInbox; diff --git a/shared/src/i18n/he/notif.ts b/shared/src/i18n/he/notif.ts index 141f00f99..73c323df2 100644 --- a/shared/src/i18n/he/notif.ts +++ b/shared/src/i18n/he/notif.ts @@ -60,7 +60,9 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} נוספה אל {trip}', 'notif.mail_booking_added.shared': '{actor} הוסיף/ה את {booking} אל {trip} מדוא״ל שהועבר', 'notif.mail_booking_pending.title': 'הזמנות שהועברו לבדיקה', - 'notif.mail_booking_pending.text': 'הזמנות שהועברו וממתינות לכם: {count}', + 'notif.mail_booking_pending.text': '{count} הזמנות שהועברו ממתינות לכם', + 'notif.mail_booking_pending.text.one': '{count} הזמנה שהועברה ממתינה לכם', + 'notif.mail_booking_pending.text.two': '{count} הזמנות שהועברו ממתינות לכם', 'notif.mail_ingest_paused.title': 'ייבוא הדואר מושהה', 'notif.mail_ingest_paused.text': 'הודעות הזמנה שהועברו אינן מתקבלות כרגע. הכתובות שלכם נשמרות ויעבדו שוב כשהייבוא יחזור.', diff --git a/shared/src/i18n/he/reservations.ts b/shared/src/i18n/he/reservations.ts index ec109ab27..296c4f509 100644 --- a/shared/src/i18n/he/reservations.ts +++ b/shared/src/i18n/he/reservations.ts @@ -37,7 +37,10 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'קונקשן', 'reservations.layover.layover': 'עצירת ביניים', 'reservations.needsReview': 'לבדיקה', - 'reservations.needsReviewHint': 'לא ניתן היה להתאים את שדה התעופה אוטומטית — אשרו את המיקום.', + 'reservations.needsReviewHint': 'חלק מהפרטים מולאו אוטומטית ולא ניתן היה לקרוא אותם בוודאות. אנא בדקו אותם.', + 'reservations.reviewConfirm': 'נראה תקין', + 'reservations.reviewConfirmHint': 'לחצו כדי לסמן כנבדק.', + 'reservations.reviewConfirmed': 'סומן כנבדק', 'reservations.searchLocation': 'חיפוש תחנה, נמל, כתובת…', 'reservations.meta.trainNumber': 'מס׳ רכבת', 'reservations.meta.platform': 'רציף', diff --git a/shared/src/i18n/he/settings.ts b/shared/src/i18n/he/settings.ts index 12646ffb9..600a8b685 100644 --- a/shared/src/i18n/he/settings.ts +++ b/shared/src/i18n/he/settings.ts @@ -124,6 +124,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'עיכובי טיסות', 'settings.notifyFlightLanded': 'נחיתות', 'settings.notifyTripReminder': 'תזכורות לטיולים', + 'settings.notifyMailBookingAdded': 'הזמנות שנוספו מדוא״ל', + 'settings.notifyMailBookingPending': 'הזמנות שהועברו וממתינות לבדיקה', + 'settings.notifyMailIngestPaused': 'ייבוא הדוא״ל הושהה', 'settings.notifyTodoDue': 'משימות שמועדן מתקרב', 'settings.notifyVacayInvite': 'הזמנות לאיחוד חופשות', 'settings.notifyVacayShare': 'שיתופי לוח חופשות', diff --git a/shared/src/i18n/hu/index.ts b/shared/src/i18n/hu/index.ts index 2a7b65d09..2a9d5857e 100644 --- a/shared/src/i18n/hu/index.ts +++ b/shared/src/i18n/hu/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/hu/mailAdmin.ts b/shared/src/i18n/hu/mailAdmin.ts new file mode 100644 index 000000000..f3d142346 --- /dev/null +++ b/shared/src/i18n/hu/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Foglalási e-mailek importja', + 'mailAdmin.hint': 'A továbbított foglalási visszaigazolásokból foglalás lesz a megfelelő utazásban.', + 'mailAdmin.enabled': 'Foglalási e-mailek importálása', + 'mailAdmin.enabledHint': + 'Mindenkinek ad egy továbbítási címet ezen a telepítésen. Kikapcsolva egyik cím sem fogad levelet.', + 'mailAdmin.transport.label': 'Hogyan érkezik a levél', + 'mailAdmin.transport.off': 'Csak feltöltés', + 'mailAdmin.transport.imap': 'IMAP-postafiók', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'AI-e-mailek személyenként és naponta', + 'mailAdmin.aiDailyHint': 'Efölött csak a géppel olvasható foglalási adatokat olvassa el.', + 'mailAdmin.template': 'Címsablon', + 'mailAdmin.templateHint': 'A továbbítási cím, ahol {token} áll, ott kerül be az egyes személyek kódja.', + 'mailAdmin.imap.host': 'Szerver', + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Titkosítás', + 'mailAdmin.imap.user': 'Felhasználó', + 'mailAdmin.imap.pass': 'Jelszó', + 'mailAdmin.imap.mailbox': 'Mappa', + 'mailAdmin.imap.mailboxHint': 'Az elolvasott levelek a TREK/processed mappába kerülnek.', + 'mailAdmin.webhook.secret': 'Aláírási titok', + 'mailAdmin.webhook.secretHint': + 'Legalább 32 karakter; a küldő ezzel írja alá az összes levelet. Üresen a webhook ki van kapcsolva.', + 'mailAdmin.inboundUrl': 'Webhook-cím', + 'mailAdmin.inboundUrlHint': 'A levelezőszolgáltatásod ide küldi az egyes leveleket, nyersen és aláírva.', + 'mailAdmin.copyUrl': 'Cím másolása', + 'mailAdmin.envHint': 'A környezeti változók (MAIL_INGEST_*) felülírják ezeket a mezőket.', + 'mailAdmin.loadError': 'Az e-mail-import beállításai nem tölthetők be.', + 'mailAdmin.problem.secretShort': 'A titoknak legalább 32 karakter hosszúnak kell lennie.', + 'mailAdmin.problem.portInvalid': 'Adj meg egy portot 1 és 65535 között.', + 'mailAdmin.problem.templateToken': 'A címben szerepelnie kell a {token} helyőrzőnek az egyes személyek kódjához.', + 'mailAdmin.problem.dailyInvalid': 'Adj meg egy egész számot 0 és 1000 között.', + 'mailAdmin.status.title': 'Állapot', + 'mailAdmin.status.lastRead': 'Utolsó olvasás: {time}', + 'mailAdmin.status.never': 'soha', + 'mailAdmin.status.mails': 'E-mailek, 24 óra', + 'mailAdmin.status.added': 'Magától hozzáadva', + 'mailAdmin.status.waiting': 'Várakozik', + 'mailAdmin.status.failed': 'Sikertelen', + 'mailAdmin.test.run': 'Kapcsolat tesztelése', + 'mailAdmin.test.ok': 'Csatlakozva', + 'mailAdmin.test.failed': 'Nincs kapcsolat', +}; +export default mailAdmin; diff --git a/shared/src/i18n/hu/mailInbox.ts b/shared/src/i18n/hu/mailInbox.ts new file mode 100644 index 000000000..cb8b07df3 --- /dev/null +++ b/shared/src/i18n/hu/mailInbox.ts @@ -0,0 +1,99 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Függőben lévő foglalások', + 'mailInbox.pendingCount': '{count} foglalás vár rád', + 'mailInbox.pendingCount.one': '{count} foglalás vár rád', + 'mailInbox.empty': 'Nincs semmi függőben', + 'mailInbox.emptyHint': + 'Azok a foglalások jelennek meg itt, amelyeket a TREK nem tudott magától hozzáadni, mellettük a lehetséges teendőkkel.', + 'mailInbox.untitled': 'Foglalási e-mail', + 'mailInbox.showMail': 'E-mail megjelenítése', + 'mailInbox.backToList': 'Vissza a listához', + 'mailInbox.loadMore': 'Továbbiak megjelenítése', + 'mailInbox.pickTrip': 'Melyik utazás?', + 'mailInbox.added': 'Foglalás hozzáadva', + 'mailInbox.filed': 'Mentve az utazás fájljai közé', + 'mailInbox.discarded': 'Elvetve', + 'mailInbox.actionError': 'Ez nem sikerült. Próbáld újra egy pillanat múlva.', + 'mailInbox.previewError': 'Az e-mail nem jeleníthető meg.', + 'mailInbox.action.add': 'Hozzáadás', + 'mailInbox.action.choose': 'Hozzáadás egy utazáshoz', + 'mailInbox.action.file': 'Mentés dokumentumként', + 'mailInbox.action.manual': 'Kitöltés kézzel', + 'mailInbox.action.discard': 'Elvetés', + 'mailInbox.reason.forwardingConfirmation': + 'Az e-mail-szolgáltatód megerősítést kér az átirányításhoz. Add meg ott ezt a kódot.', + 'mailInbox.reason.newsletter': 'Hírlevél, nem foglalás', + 'mailInbox.reason.autoReply': 'Automatikus válasz', + 'mailInbox.reason.emptyMail': 'Az e-mailben nincs olvasható szöveg vagy melléklet', + 'mailInbox.reason.mailUnreadable': 'Az e-mail nem olvasható', + 'mailInbox.reason.notABooking': 'Ebben az e-mailben nem található foglalás', + 'mailInbox.reason.tooLarge': 'Az e-mail túl nagy az olvasáshoz', + 'mailInbox.reason.addressInvalid': 'Olyan címre érkezett, amely már nem működik', + 'mailInbox.reason.addressDailyLimit': 'A címed ma már elfogadta az összes levelét', + 'mailInbox.reason.processingFailed': 'A feldolgozás többször sikertelen volt', + 'mailInbox.reason.aiQuotaExhausted': 'Ennek az időszaknak az AI-kerete elfogyott', + 'mailInbox.reason.aiDisabled': 'AI nélkül olvasva, így nem talált foglalást', + 'mailInbox.reason.aiUserCap': 'Elérted az AI-val olvasott e-mailek napi korlátját', + 'mailInbox.reason.aiProviderError': 'Az AI most nem tudta elolvasni', + 'mailInbox.reason.noTripInRange': 'Egyetlen utazás sem fedi le ezeket a dátumokat', + 'mailInbox.reason.tripAmbiguous': 'Több utazásról is szó lehet', + 'mailInbox.reason.tripWithoutDates': 'Az utazásnak nincsenek dátumai az egyeztetéshez', + 'mailInbox.reason.noEditPermission': 'Nincs jogod foglalást hozzáadni ehhez az utazáshoz', + 'mailInbox.reason.demoMode': 'A demó nem ad hozzá foglalásokat', + 'mailInbox.reason.outsideTripDates': 'Az egyik dátum az utazáson kívül esik', + 'mailInbox.reason.sourceMismatch': 'Az e-mail adatai ellentmondanak egymásnak', + 'mailInbox.reason.implausible': 'Egy érték nem lehet helyes', + 'mailInbox.reason.notInText': 'Egy kiolvasott érték nincs benne az e-mailben', + 'mailInbox.reason.imageOnly': 'Csak képből olvasva, ezért nézd át te is', + 'mailInbox.reason.missingCriticalField': 'Hiányzik a típus, a dátum vagy az útvonal', + 'mailInbox.reason.missingConfirmation': 'Nem található foglalási azonosító', + 'mailInbox.reason.tooManyBookings': 'Egy e-mailben több foglalás van, mint amennyit magától hozzáad', + 'mailInbox.reason.changeOrCancellation': 'Egy korábbi foglalás módosítása vagy lemondása', + 'mailInbox.reason.cancelled': 'A foglalás le van mondva', + 'mailInbox.reason.possibleDuplicate': 'Lehet, hogy az utazásban már szerepel ez a foglalás', + 'mailInbox.reason.changeAvailable': 'Az utazásban már szerepel ez a foglalás, és az e-mail módosítja', + 'mailInbox.reason.conflict': 'Az utazásban ez a foglalás más adatokkal szerepel', + 'mailInbox.reason.askFirst': 'Azt kérted, hogy minden foglalást megerősíthess', + 'mailInbox.settings.title': 'Foglalási e-mailek', + 'mailInbox.settings.on': 'Be', + 'mailInbox.settings.off': 'Ki', + 'mailInbox.settings.yourAddress': 'A címed', + 'mailInbox.settings.copy': 'Cím másolása', + 'mailInbox.settings.copyError': 'A másolás nem sikerült', + 'mailInbox.settings.renew': 'Új cím', + 'mailInbox.settings.renewConfirm': 'Koppints újra: a régi cím megszűnik működni', + 'mailInbox.settings.renewed': 'Az új címed kész', + 'mailInbox.settings.saveError': 'A mentés nem sikerült', + 'mailInbox.settings.addressHint': + 'Küldd erre a címre a foglalási visszaigazolásokat, és a TREK hozzáadja őket a megfelelő utazáshoz. Tartsd titokban, mint egy jelszót.', + 'mailInbox.settings.howTo': 'Automatikus átirányítás', + 'mailInbox.settings.howToGmail': + 'Gmail: Beállítások, Továbbítás és POP/IMAP, Továbbítási cím hozzáadása. A Gmail küld egy kódot, amely a Függőben lévő foglalások alatt jelenik meg. Utána adj hozzá egy szűrőt a foglalási feladókhoz, amely a TREK címére továbbít.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Beállítások, Levelezés, Szabályok, adj hozzá egy szabályt, amely továbbítja a foglalási e-maileket. Néhány munkahelyi fiók blokkolja az automatikus továbbítást, ott kézzel továbbítsd.', + 'mailInbox.settings.howToManual': 'Kézzel: továbbíts bármilyen foglalási e-mailt a mellékletekkel együtt.', + 'mailInbox.settings.enabled': 'Foglalási e-mailek importálása', + 'mailInbox.settings.enabledHint': 'Kikapcsolva a címedre érkező e-maileket nem olvassa el.', + 'mailInbox.settings.ai': 'Olvasás AI-val', + 'mailInbox.settings.aiHint': + 'Bármilyen foglalási e-mailt elolvas, és megtalálja hozzá az utazást. Az e-mail, valamint az utazásaid dátumai és úti céljai az AI-szolgáltatóhoz kerülnek.', + 'mailInbox.settings.aiUnavailable': + 'Itt nem érhető el: csak a géppel olvasható foglalási adatokat tartalmazó e-maileket olvassa el.', + 'mailInbox.settings.autoAdd': 'Hozzáadás magától', + 'mailInbox.settings.autoAddHint': + 'Az a foglalás, amelyben a TREK biztos, rögtön bekerül az utazásába, és 7 napig visszavonhatod. Kikapcsolva mindegyik a te jóváhagyásodra vár.', + 'mailInbox.settings.upload': 'E-mail-fájl feltöltése', + 'mailInbox.settings.uploaded': 'E-mail megérkezett, feldolgozás folyamatban', + 'mailInbox.settings.uploadDuplicate': 'Ez az e-mail már megvan', + 'mailInbox.settings.uploadError': 'A fájl nem olvasható e-mailként', + 'mailInbox.settings.privacy': + 'A továbbított e-maileket titkosítva őrizzük legfeljebb 30 napig, majd töröljük. Kérjük, ne továbbíts személyi igazolványt vagy egészségügyi dokumentumokat.', + 'mailInbox.trip.title': 'Vagy továbbítás e-mailben', + 'mailInbox.trip.hint': 'Az utazás címére küldött foglalási e-mail találgatás nélkül ide kerül.', + 'mailInbox.trip.create': 'Cím létrehozása ehhez az utazáshoz', + 'mailInbox.fromMail': 'E-mailből', + 'mailInbox.fromMailHint': 'Továbbított foglalási e-mailből hozzáadva', +}; +export default mailInbox; diff --git a/shared/src/i18n/hu/notif.ts b/shared/src/i18n/hu/notif.ts index a04da5c06..7c9c8eeb6 100644 --- a/shared/src/i18n/hu/notif.ts +++ b/shared/src/i18n/hu/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} hozzáadva ehhez: {trip}', 'notif.mail_booking_added.shared': '{actor} hozzáadta: {booking}, ehhez: {trip}, egy továbbított levélből', 'notif.mail_booking_pending.title': 'Ellenőrizendő továbbított foglalások', - 'notif.mail_booking_pending.text': 'Rád váró továbbított foglalások: {count}', + 'notif.mail_booking_pending.text': '{count} továbbított foglalás vár rád', + 'notif.mail_booking_pending.text.one': '{count} továbbított foglalás vár rád', 'notif.mail_ingest_paused.title': 'E-mail-importálás szüneteltetve', 'notif.mail_ingest_paused.text': 'A továbbított foglalási e-maileket egyelőre nem fogadjuk. A címeid megmaradnak, és újra működnek, amint az importálás visszatér.', diff --git a/shared/src/i18n/hu/reservations.ts b/shared/src/i18n/hu/reservations.ts index 9a9143d73..5107c3c8b 100644 --- a/shared/src/i18n/hu/reservations.ts +++ b/shared/src/i18n/hu/reservations.ts @@ -37,7 +37,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Csatlakozás', 'reservations.layover.layover': 'Átszállás', 'reservations.needsReview': 'Ellenőrzés', - 'reservations.needsReviewHint': 'A repülőteret nem sikerült automatikusan azonosítani — erősítsd meg a helyet.', + 'reservations.needsReviewHint': + 'Néhány adat automatikusan lett kitöltve, és nem olvasható ki biztosan. Kérjük, ellenőrizd őket.', + 'reservations.reviewConfirm': 'Rendben van', + 'reservations.reviewConfirmHint': 'Kattints, hogy ellenőrzöttként jelöld meg.', + 'reservations.reviewConfirmed': 'Ellenőrzöttként megjelölve', 'reservations.searchLocation': 'Állomás, kikötő, cím keresése...', 'reservations.meta.trainNumber': 'Vonatszám', 'reservations.meta.platform': 'Vágány', diff --git a/shared/src/i18n/hu/settings.ts b/shared/src/i18n/hu/settings.ts index 15cb17128..844785caa 100644 --- a/shared/src/i18n/hu/settings.ts +++ b/shared/src/i18n/hu/settings.ts @@ -116,6 +116,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Járatkésések', 'settings.notifyFlightLanded': 'Leszállások', 'settings.notifyTripReminder': 'Utazási emlékeztetők', + 'settings.notifyMailBookingAdded': 'E-mailből hozzáadott foglalások', + 'settings.notifyMailBookingPending': 'Átnézésre váró továbbított foglalások', + 'settings.notifyMailIngestPaused': 'E-mail-import szüneteltetve', 'settings.notifyTodoDue': 'Teendő esedékes', 'settings.notifyVacayInvite': 'Vacay összevonási meghívók', 'settings.notifyVacayShare': 'Vacay naptármegosztások', diff --git a/shared/src/i18n/id/index.ts b/shared/src/i18n/id/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/id/index.ts +++ b/shared/src/i18n/id/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/id/mailAdmin.ts b/shared/src/i18n/id/mailAdmin.ts new file mode 100644 index 000000000..95254172c --- /dev/null +++ b/shared/src/i18n/id/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Impor email pemesanan', + 'mailAdmin.hint': 'Konfirmasi pemesanan yang diteruskan menjadi pemesanan di perjalanan yang tepat.', + 'mailAdmin.enabled': 'Impor email pemesanan', + 'mailAdmin.enabledHint': + 'Memberi semua orang di instalasi ini alamat penerusan. Jika nonaktif, tidak ada alamat yang menerima email.', + 'mailAdmin.transport.label': 'Cara email masuk', + 'mailAdmin.transport.off': 'Hanya unggah', + 'mailAdmin.transport.imap': 'Kotak surat IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Email AI per orang per hari', + 'mailAdmin.aiDailyHint': 'Di atas batas ini, hanya data pemesanan yang dapat dibaca mesin yang dibaca.', + 'mailAdmin.template': 'Templat alamat', + 'mailAdmin.templateHint': 'Alamat penerusan, dengan {token} di tempat kode setiap orang dimasukkan.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Enkripsi', + 'mailAdmin.imap.user': 'Pengguna', + 'mailAdmin.imap.pass': 'Kata sandi', + 'mailAdmin.imap.mailbox': 'Folder', // same-as-en + 'mailAdmin.imap.mailboxHint': 'Email yang sudah dibaca dipindahkan ke TREK/processed.', + 'mailAdmin.webhook.secret': 'Rahasia penandatanganan', + 'mailAdmin.webhook.secretHint': + 'Minimal 32 karakter; pengirim menandatangani setiap email dengannya. Jika kosong, webhook dinonaktifkan.', + 'mailAdmin.inboundUrl': 'Alamat webhook', + 'mailAdmin.inboundUrlHint': 'Layanan email Anda mengirim setiap email ke sini, mentah dan bertanda tangan.', + 'mailAdmin.copyUrl': 'Salin alamat', + 'mailAdmin.envHint': 'Variabel lingkungan (MAIL_INGEST_*) lebih diutamakan daripada kolom ini.', + 'mailAdmin.loadError': 'Pengaturan impor email tidak dapat dimuat.', + 'mailAdmin.problem.secretShort': 'Rahasia minimal 32 karakter.', + 'mailAdmin.problem.portInvalid': 'Masukkan port antara 1 dan 65535.', + 'mailAdmin.problem.templateToken': 'Alamat memerlukan {token} untuk kode setiap orang.', + 'mailAdmin.problem.dailyInvalid': 'Masukkan bilangan bulat dari 0 sampai 1000.', + 'mailAdmin.status.title': 'Status', // same-as-en + 'mailAdmin.status.lastRead': 'Terakhir dibaca {time}', + 'mailAdmin.status.never': 'belum pernah', + 'mailAdmin.status.mails': 'Email, 24 j', + 'mailAdmin.status.added': 'Ditambahkan otomatis', + 'mailAdmin.status.waiting': 'Menunggu', + 'mailAdmin.status.failed': 'Gagal', + 'mailAdmin.test.run': 'Uji koneksi', + 'mailAdmin.test.ok': 'Terhubung', + 'mailAdmin.test.failed': 'Tidak terhubung', +}; +export default mailAdmin; diff --git a/shared/src/i18n/id/mailInbox.ts b/shared/src/i18n/id/mailInbox.ts new file mode 100644 index 000000000..4fe7b0408 --- /dev/null +++ b/shared/src/i18n/id/mailInbox.ts @@ -0,0 +1,98 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Pemesanan tertunda', + 'mailInbox.pendingCount': '{count} pemesanan menunggu Anda', + 'mailInbox.empty': 'Tidak ada yang menunggu', + 'mailInbox.emptyHint': + 'Pemesanan yang tidak bisa ditambahkan TREK secara otomatis muncul di sini, beserta pilihan tindakan untuk masing-masing.', + 'mailInbox.untitled': 'Email pemesanan', + 'mailInbox.showMail': 'Tampilkan email', + 'mailInbox.backToList': 'Kembali ke daftar', + 'mailInbox.loadMore': 'Tampilkan lebih banyak', + 'mailInbox.pickTrip': 'Perjalanan yang mana?', + 'mailInbox.added': 'Pemesanan ditambahkan', + 'mailInbox.filed': 'Disimpan ke file perjalanan', + 'mailInbox.discarded': 'Dibuang', + 'mailInbox.actionError': 'Gagal. Coba lagi sebentar lagi.', + 'mailInbox.previewError': 'Email tidak dapat ditampilkan.', + 'mailInbox.action.add': 'Tambahkan', + 'mailInbox.action.choose': 'Tambahkan ke perjalanan', + 'mailInbox.action.file': 'Simpan sebagai dokumen', + 'mailInbox.action.manual': 'Isi sendiri', + 'mailInbox.action.discard': 'Buang', + 'mailInbox.reason.forwardingConfirmation': + 'Penyedia email Anda meminta konfirmasi penerusan. Masukkan kode ini di sana.', + 'mailInbox.reason.newsletter': 'Buletin, bukan pemesanan', + 'mailInbox.reason.autoReply': 'Balasan otomatis', + 'mailInbox.reason.emptyMail': 'Email tidak berisi teks atau lampiran yang bisa dibaca', + 'mailInbox.reason.mailUnreadable': 'Email tidak dapat dibaca', + 'mailInbox.reason.notABooking': 'Tidak ada pemesanan di email ini', + 'mailInbox.reason.tooLarge': 'Email terlalu besar untuk dibaca', + 'mailInbox.reason.addressInvalid': 'Masuk ke alamat yang sudah tidak berfungsi', + 'mailInbox.reason.addressDailyLimit': 'Alamat Anda sudah menerima semua email jatahnya hari ini', + 'mailInbox.reason.processingFailed': 'Pembacaan gagal beberapa kali', + 'mailInbox.reason.aiQuotaExhausted': 'Kuota AI periode ini sudah habis', + 'mailInbox.reason.aiDisabled': 'Dibaca tanpa AI, yang tidak menemukan pemesanan', + 'mailInbox.reason.aiUserCap': 'Batas harian email yang dibaca AI sudah tercapai', + 'mailInbox.reason.aiProviderError': 'AI tidak dapat membacanya saat ini', + 'mailInbox.reason.noTripInRange': 'Tidak ada perjalanan yang mencakup tanggal ini', + 'mailInbox.reason.tripAmbiguous': 'Bisa jadi yang dimaksud lebih dari satu perjalanan', + 'mailInbox.reason.tripWithoutDates': 'Perjalanan tidak punya tanggal untuk dicocokkan', + 'mailInbox.reason.noEditPermission': 'Anda tidak boleh menambahkan pemesanan ke perjalanan itu', + 'mailInbox.reason.demoMode': 'Demo tidak menambahkan pemesanan', + 'mailInbox.reason.outsideTripDates': 'Ada tanggal di luar perjalanan', + 'mailInbox.reason.sourceMismatch': 'Detail di email saling bertentangan', + 'mailInbox.reason.implausible': 'Ada nilai yang tidak mungkin benar', + 'mailInbox.reason.notInText': 'Ada nilai hasil bacaan yang tidak ada di email', + 'mailInbox.reason.imageOnly': 'Dibaca hanya dari gambar, jadi perlu Anda periksa', + 'mailInbox.reason.missingCriticalField': 'Jenis, tanggal, atau rute tidak ada', + 'mailInbox.reason.missingConfirmation': 'Kode pemesanan tidak ditemukan', + 'mailInbox.reason.tooManyBookings': 'Satu email berisi lebih banyak pemesanan daripada yang ditambahkan otomatis', + 'mailInbox.reason.changeOrCancellation': 'Perubahan atau pembatalan pemesanan sebelumnya', + 'mailInbox.reason.cancelled': 'Pemesanan dibatalkan', + 'mailInbox.reason.possibleDuplicate': 'Perjalanan mungkin sudah memuat pemesanan ini', + 'mailInbox.reason.changeAvailable': 'Perjalanan sudah memuat pemesanan ini, dan email mengubahnya', + 'mailInbox.reason.conflict': 'Perjalanan memuat pemesanan ini dengan detail yang berbeda', + 'mailInbox.reason.askFirst': 'Anda memilih untuk mengonfirmasi setiap pemesanan', + 'mailInbox.settings.title': 'Email pemesanan', + 'mailInbox.settings.on': 'Aktif', + 'mailInbox.settings.off': 'Nonaktif', + 'mailInbox.settings.yourAddress': 'Alamat Anda', + 'mailInbox.settings.copy': 'Salin alamat', + 'mailInbox.settings.copyError': 'Tidak dapat menyalin', + 'mailInbox.settings.renew': 'Alamat baru', + 'mailInbox.settings.renewConfirm': 'Ketuk lagi: alamat lama akan berhenti berfungsi', + 'mailInbox.settings.renewed': 'Alamat baru Anda sudah siap', + 'mailInbox.settings.saveError': 'Tidak dapat menyimpan', + 'mailInbox.settings.addressHint': + 'Teruskan konfirmasi pemesanan ke alamat ini dan TREK akan menambahkannya ke perjalanan yang tepat. Rahasiakan seperti kata sandi.', + 'mailInbox.settings.howTo': 'Teruskan otomatis', + 'mailInbox.settings.howToGmail': + 'Gmail: Setelan, Penerusan dan POP/IMAP, Tambahkan alamat penerusan. Gmail mengirim kode, yang muncul di Pemesanan tertunda. Lalu tambahkan filter untuk pengirim pemesanan yang meneruskan ke TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Setelan, Email, Aturan, tambahkan aturan yang meneruskan email pemesanan. Beberapa akun kantor memblokir penerusan otomatis; teruskan secara manual di sana.', + 'mailInbox.settings.howToManual': 'Manual: teruskan email pemesanan apa pun, termasuk lampirannya.', + 'mailInbox.settings.enabled': 'Impor email pemesanan', + 'mailInbox.settings.enabledHint': 'Jika nonaktif, email ke alamat Anda tidak dibaca.', + 'mailInbox.settings.ai': 'Baca dengan AI', + 'mailInbox.settings.aiHint': + 'Membaca email pemesanan apa pun dan menemukan perjalanannya. Email serta tanggal dan tujuan perjalanan Anda dikirim ke penyedia AI.', + 'mailInbox.settings.aiUnavailable': + 'Tidak tersedia di sini: hanya email dengan data pemesanan yang dapat dibaca mesin yang dibaca.', + 'mailInbox.settings.autoAdd': 'Tambahkan otomatis', + 'mailInbox.settings.autoAddHint': + 'Pemesanan yang diyakini TREK langsung masuk ke perjalanannya, dan Anda bisa membatalkannya selama 7 hari. Jika nonaktif, setiap pemesanan menunggu persetujuan Anda.', + 'mailInbox.settings.upload': 'Unggah file email', + 'mailInbox.settings.uploaded': 'Email diterima, sedang dibaca', + 'mailInbox.settings.uploadDuplicate': 'Email ini sudah ada', + 'mailInbox.settings.uploadError': 'File tidak dapat dibaca sebagai email', + 'mailInbox.settings.privacy': + 'Email yang diteruskan disimpan terenkripsi paling lama 30 hari lalu dihapus. Jangan teruskan dokumen identitas atau kesehatan.', + 'mailInbox.trip.title': 'Atau teruskan lewat email', + 'mailInbox.trip.hint': 'Email pemesanan yang dikirim ke alamat perjalanan ini masuk ke sini tanpa tebakan.', + 'mailInbox.trip.create': 'Buat alamat untuk perjalanan ini', + 'mailInbox.fromMail': 'Dari email', + 'mailInbox.fromMailHint': 'Ditambahkan dari email pemesanan yang diteruskan', +}; +export default mailInbox; diff --git a/shared/src/i18n/id/notif.ts b/shared/src/i18n/id/notif.ts index f57de8acc..a2de41e41 100644 --- a/shared/src/i18n/id/notif.ts +++ b/shared/src/i18n/id/notif.ts @@ -58,7 +58,7 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} ditambahkan ke {trip}', 'notif.mail_booking_added.shared': '{actor} menambahkan {booking} ke {trip} dari email yang diteruskan', 'notif.mail_booking_pending.title': 'Pemesanan terusan untuk ditinjau', - 'notif.mail_booking_pending.text': 'Pemesanan terusan yang menunggumu: {count}', + 'notif.mail_booking_pending.text': '{count} pemesanan terusan menunggu Anda', 'notif.mail_ingest_paused.title': 'Impor email dijeda', 'notif.mail_ingest_paused.text': 'Email pemesanan yang diteruskan untuk sementara tidak diterima. Alamatmu tetap ada dan akan berfungsi lagi saat impor kembali.', diff --git a/shared/src/i18n/id/reservations.ts b/shared/src/i18n/id/reservations.ts index fd05790f9..10efcefe5 100644 --- a/shared/src/i18n/id/reservations.ts +++ b/shared/src/i18n/id/reservations.ts @@ -36,7 +36,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Sambungan', 'reservations.layover.layover': 'Transit', 'reservations.needsReview': 'Tinjau', - 'reservations.needsReviewHint': 'Bandara tidak dapat dicocokkan otomatis — konfirmasi lokasi.', + 'reservations.needsReviewHint': + 'Beberapa detail terisi otomatis dan tidak dapat dibaca dengan pasti. Silakan periksa.', + 'reservations.reviewConfirm': 'Sudah benar', + 'reservations.reviewConfirmHint': 'Klik untuk menandai sebagai sudah diperiksa.', + 'reservations.reviewConfirmed': 'Ditandai sudah diperiksa', 'reservations.searchLocation': 'Cari stasiun, pelabuhan, alamat...', 'reservations.meta.trainNumber': 'No. Kereta', 'reservations.meta.platform': 'Peron', diff --git a/shared/src/i18n/id/settings.ts b/shared/src/i18n/id/settings.ts index ba0bbc20b..69b917747 100644 --- a/shared/src/i18n/id/settings.ts +++ b/shared/src/i18n/id/settings.ts @@ -113,6 +113,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Penerbangan terlambat', 'settings.notifyFlightLanded': 'Penerbangan mendarat', 'settings.notifyTripReminder': 'Pengingat perjalanan', + 'settings.notifyMailBookingAdded': 'Pemesanan yang ditambahkan dari email', + 'settings.notifyMailBookingPending': 'Pemesanan terusan yang perlu ditinjau', + 'settings.notifyMailIngestPaused': 'Impor email dijeda', 'settings.notifyTodoDue': 'Tugas jatuh tempo', 'settings.notifyVacayInvite': 'Undangan Vacay fusion', 'settings.notifyVacayShare': 'Berbagi kalender Vacay', diff --git a/shared/src/i18n/it/index.ts b/shared/src/i18n/it/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/it/index.ts +++ b/shared/src/i18n/it/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/it/mailAdmin.ts b/shared/src/i18n/it/mailAdmin.ts new file mode 100644 index 000000000..8479218d8 --- /dev/null +++ b/shared/src/i18n/it/mailAdmin.ts @@ -0,0 +1,48 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Importazione delle e-mail di prenotazione', + 'mailAdmin.hint': 'Le conferme di prenotazione inoltrate diventano prenotazioni nel viaggio giusto.', + 'mailAdmin.enabled': 'Importa e-mail di prenotazione', + 'mailAdmin.enabledHint': + 'Dà a tutti su questa installazione un indirizzo di inoltro. Disattivato, nessun indirizzo riceve posta.', + 'mailAdmin.transport.label': 'Come arriva la posta', + 'mailAdmin.transport.off': 'Solo caricamento di file', + 'mailAdmin.transport.imap': 'Casella IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'E-mail con IA per persona al giorno', + 'mailAdmin.aiDailyHint': + 'Oltre questo numero, vengono lette solo le e-mail con dati di prenotazione leggibili da una macchina.', + 'mailAdmin.template': 'Modello di indirizzo', + 'mailAdmin.templateHint': 'L’indirizzo di inoltro, con {token} dove va il codice di ogni persona.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Porta', + 'mailAdmin.imap.tls': 'Crittografia', + 'mailAdmin.imap.user': 'Utente', + 'mailAdmin.imap.pass': 'Password', // same-as-en + 'mailAdmin.imap.mailbox': 'Cartella', + 'mailAdmin.imap.mailboxHint': 'Le e-mail lette vengono spostate in TREK/processed.', + 'mailAdmin.webhook.secret': 'Segreto di firma', + 'mailAdmin.webhook.secretHint': + 'Almeno 32 caratteri; il mittente firma ogni e-mail con esso. Vuoto, il webhook è disattivato.', + 'mailAdmin.inboundUrl': 'Indirizzo del webhook', + 'mailAdmin.inboundUrlHint': 'Il tuo servizio di posta invia qui ogni e-mail, grezza e firmata.', + 'mailAdmin.copyUrl': 'Copia indirizzo', + 'mailAdmin.envHint': 'Le variabili d’ambiente (MAIL_INGEST_*) hanno la precedenza su questi campi.', + 'mailAdmin.loadError': 'Impossibile caricare le impostazioni di importazione delle e-mail.', + 'mailAdmin.problem.secretShort': 'Il segreto deve avere almeno 32 caratteri.', + 'mailAdmin.problem.portInvalid': 'Inserisci una porta tra 1 e 65535.', + 'mailAdmin.problem.templateToken': 'L’indirizzo richiede {token} per il codice di ogni persona.', + 'mailAdmin.problem.dailyInvalid': 'Inserisci un numero intero da 0 a 1000.', + 'mailAdmin.status.title': 'Stato', + 'mailAdmin.status.lastRead': 'Ultima lettura {time}', + 'mailAdmin.status.never': 'mai', + 'mailAdmin.status.mails': 'E-mail, 24 h', + 'mailAdmin.status.added': 'Aggiunte automaticamente', + 'mailAdmin.status.waiting': 'In attesa', + 'mailAdmin.status.failed': 'Non riuscite', + 'mailAdmin.test.run': 'Verifica connessione', + 'mailAdmin.test.ok': 'Connesso', + 'mailAdmin.test.failed': 'Nessuna connessione', +}; +export default mailAdmin; diff --git a/shared/src/i18n/it/mailInbox.ts b/shared/src/i18n/it/mailInbox.ts new file mode 100644 index 000000000..dcea46442 --- /dev/null +++ b/shared/src/i18n/it/mailInbox.ts @@ -0,0 +1,101 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Prenotazioni in sospeso', + 'mailInbox.pendingCount': '{count} prenotazioni ti aspettano', + 'mailInbox.pendingCount.one': '{count} prenotazione ti aspetta', + 'mailInbox.empty': 'Niente in sospeso', + 'mailInbox.emptyHint': + 'Le prenotazioni che TREK non ha potuto aggiungere da solo compaiono qui, con quello che puoi fare per ciascuna.', + 'mailInbox.untitled': 'E-mail di prenotazione', + 'mailInbox.showMail': 'Mostra e-mail', + 'mailInbox.backToList': 'Torna all’elenco', + 'mailInbox.loadMore': 'Mostra altro', + 'mailInbox.pickTrip': 'Quale viaggio?', + 'mailInbox.added': 'Prenotazione aggiunta', + 'mailInbox.filed': 'Salvato nei file del viaggio', + 'mailInbox.discarded': 'Scartato', + 'mailInbox.actionError': 'Non ha funzionato. Riprova tra un attimo.', + 'mailInbox.previewError': 'Impossibile mostrare l’e-mail.', + 'mailInbox.action.add': 'Aggiungi', + 'mailInbox.action.choose': 'Aggiungi a un viaggio', + 'mailInbox.action.file': 'Salva come documento', + 'mailInbox.action.manual': 'Compila tu stesso', + 'mailInbox.action.discard': 'Scarta', + 'mailInbox.reason.forwardingConfirmation': + 'Il tuo provider di posta chiede di confermare l’inoltro. Inserisci lì questo codice.', + 'mailInbox.reason.newsletter': 'Una newsletter, non una prenotazione', + 'mailInbox.reason.autoReply': 'Una risposta automatica', + 'mailInbox.reason.emptyMail': 'L’e-mail non ha testo né allegati da leggere', + 'mailInbox.reason.mailUnreadable': 'Non è stato possibile leggere l’e-mail', + 'mailInbox.reason.notABooking': 'Nessuna prenotazione trovata in questa e-mail', + 'mailInbox.reason.tooLarge': 'L’e-mail è troppo grande per essere letta', + 'mailInbox.reason.addressInvalid': 'È arrivata a un indirizzo che non funziona più', + 'mailInbox.reason.addressDailyLimit': 'Il tuo indirizzo ha già ricevuto tutte le e-mail di oggi', + 'mailInbox.reason.processingFailed': 'La lettura è fallita più volte', + 'mailInbox.reason.aiQuotaExhausted': 'La quota di IA di questo periodo è esaurita', + 'mailInbox.reason.aiDisabled': 'Letta senza IA, che non ha trovato nessuna prenotazione', + 'mailInbox.reason.aiUserCap': 'Hai raggiunto il limite giornaliero di e-mail lette con IA', + 'mailInbox.reason.aiProviderError': 'L’IA non è riuscita a leggerla ora', + 'mailInbox.reason.noTripInRange': 'Nessun viaggio copre queste date', + 'mailInbox.reason.tripAmbiguous': 'Potrebbe trattarsi di più di un viaggio', + 'mailInbox.reason.tripWithoutDates': 'Il viaggio non ha date con cui confrontare', + 'mailInbox.reason.noEditPermission': 'Non puoi aggiungere prenotazioni a quel viaggio', + 'mailInbox.reason.demoMode': 'La demo non aggiunge prenotazioni', + 'mailInbox.reason.outsideTripDates': 'Una data cade fuori dal viaggio', + 'mailInbox.reason.sourceMismatch': 'I dati dell’e-mail non coincidono tra loro', + 'mailInbox.reason.implausible': 'Un valore non può essere corretto', + 'mailInbox.reason.notInText': 'Un valore letto non compare nell’e-mail', + 'mailInbox.reason.imageOnly': 'Letta solo da un’immagine, quindi va controllata da te', + 'mailInbox.reason.missingCriticalField': 'Mancano il tipo, la data o il percorso', + 'mailInbox.reason.missingConfirmation': 'Nessun codice di prenotazione trovato', + 'mailInbox.reason.tooManyBookings': + 'Più prenotazioni in una sola e-mail di quante ne vengano aggiunte automaticamente', + 'mailInbox.reason.changeOrCancellation': 'Una modifica o un annullamento di una prenotazione precedente', + 'mailInbox.reason.cancelled': 'La prenotazione è annullata', + 'mailInbox.reason.possibleDuplicate': 'Forse il viaggio contiene già questa prenotazione', + 'mailInbox.reason.changeAvailable': 'Il viaggio contiene questa prenotazione, e l’e-mail la modifica', + 'mailInbox.reason.conflict': 'Il viaggio contiene questa prenotazione con dati diversi', + 'mailInbox.reason.askFirst': 'Hai chiesto di confermare ogni prenotazione', + 'mailInbox.settings.title': 'E-mail di prenotazione', + 'mailInbox.settings.on': 'Attivo', + 'mailInbox.settings.off': 'Disattivato', + 'mailInbox.settings.yourAddress': 'Il tuo indirizzo', + 'mailInbox.settings.copy': 'Copia indirizzo', + 'mailInbox.settings.copyError': 'Impossibile copiare', + 'mailInbox.settings.renew': 'Nuovo indirizzo', + 'mailInbox.settings.renewConfirm': 'Tocca di nuovo: il vecchio indirizzo smette di funzionare', + 'mailInbox.settings.renewed': 'Il tuo nuovo indirizzo è pronto', + 'mailInbox.settings.saveError': 'Impossibile salvare', + 'mailInbox.settings.addressHint': + 'Inoltra le conferme di prenotazione a questo indirizzo e TREK le aggiunge al viaggio giusto. Tienilo riservato, come una password.', + 'mailInbox.settings.howTo': 'Inoltra automaticamente', + 'mailInbox.settings.howToGmail': + 'Gmail: Impostazioni, Inoltro e POP/IMAP, Aggiungi un indirizzo di inoltro. Gmail invia un codice, che compare in Prenotazioni in sospeso. Poi aggiungi un filtro per i mittenti delle prenotazioni che inoltri a TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Impostazioni, Posta, Regole, aggiungi una regola che inoltri le e-mail di prenotazione. Alcuni account di lavoro bloccano l’inoltro automatico; in quel caso inoltra a mano.', + 'mailInbox.settings.howToManual': 'A mano: inoltra qualsiasi e-mail di prenotazione, allegati inclusi.', + 'mailInbox.settings.enabled': 'Importa e-mail di prenotazione', + 'mailInbox.settings.enabledHint': 'Disattivato, le e-mail inviate al tuo indirizzo non vengono lette.', + 'mailInbox.settings.ai': 'Leggi con IA', + 'mailInbox.settings.aiHint': + 'Legge qualsiasi e-mail di prenotazione e ne trova il viaggio. L’e-mail e le date e destinazioni dei tuoi viaggi vengono inviate al provider di IA.', + 'mailInbox.settings.aiUnavailable': + 'Non disponibile qui: vengono lette solo le e-mail con dati di prenotazione leggibili da una macchina.', + 'mailInbox.settings.autoAdd': 'Aggiungi automaticamente', + 'mailInbox.settings.autoAddHint': + 'Una prenotazione di cui TREK è sicuro va direttamente nel suo viaggio, e puoi annullarla per 7 giorni. Disattivato, ognuna attende il tuo OK.', + 'mailInbox.settings.upload': 'Carica un file e-mail', + 'mailInbox.settings.uploaded': 'E-mail ricevuta, è in lettura', + 'mailInbox.settings.uploadDuplicate': 'Questa e-mail è già stata importata', + 'mailInbox.settings.uploadError': 'Impossibile leggere il file come e-mail', + 'mailInbox.settings.privacy': + 'Le e-mail inoltrate vengono conservate cifrate per al massimo 30 giorni e poi eliminate. Per favore, non inoltrare documenti d’identità o sanitari.', + 'mailInbox.trip.title': 'Oppure inoltra via e-mail', + 'mailInbox.trip.hint': + 'Un’e-mail di prenotazione inviata all’indirizzo di questo viaggio arriva qui senza indovinare nulla.', + 'mailInbox.trip.create': 'Crea un indirizzo per questo viaggio', + 'mailInbox.fromMail': 'Da e-mail', + 'mailInbox.fromMailHint': 'Aggiunta da un’e-mail di prenotazione inoltrata', +}; +export default mailInbox; diff --git a/shared/src/i18n/it/notif.ts b/shared/src/i18n/it/notif.ts index b1d857013..c8a101106 100644 --- a/shared/src/i18n/it/notif.ts +++ b/shared/src/i18n/it/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} è stata aggiunta a {trip}', 'notif.mail_booking_added.shared': '{actor} ha aggiunto {booking} a {trip} da un’email inoltrata', 'notif.mail_booking_pending.title': 'Prenotazioni inoltrate da controllare', - 'notif.mail_booking_pending.text': 'Prenotazioni inoltrate in attesa: {count}', + 'notif.mail_booking_pending.text': '{count} prenotazioni inoltrate ti aspettano', + 'notif.mail_booking_pending.text.one': '{count} prenotazione inoltrata ti aspetta', 'notif.mail_ingest_paused.title': 'Importazione email in pausa', 'notif.mail_ingest_paused.text': 'Per ora le email di prenotazione inoltrate non vengono accettate. I tuoi indirizzi restano e funzioneranno di nuovo quando l’importazione tornerà.', diff --git a/shared/src/i18n/it/reservations.ts b/shared/src/i18n/it/reservations.ts index c7cf59df2..8561fc412 100644 --- a/shared/src/i18n/it/reservations.ts +++ b/shared/src/i18n/it/reservations.ts @@ -36,7 +36,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Coincidenza', 'reservations.layover.layover': 'Sosta', 'reservations.needsReview': 'Verifica', - 'reservations.needsReviewHint': "L'aeroporto non è stato riconosciuto automaticamente — conferma la posizione.", + 'reservations.needsReviewHint': + 'Alcuni dati sono stati compilati automaticamente e non è stato possibile leggerli con certezza. Controllali, per favore.', + 'reservations.reviewConfirm': 'È corretto', + 'reservations.reviewConfirmHint': 'Clicca per segnare come controllato.', + 'reservations.reviewConfirmed': 'Segnato come controllato', 'reservations.searchLocation': 'Cerca stazione, porto, indirizzo...', 'reservations.meta.trainNumber': 'N. treno', 'reservations.meta.platform': 'Binario', diff --git a/shared/src/i18n/it/settings.ts b/shared/src/i18n/it/settings.ts index 7825c8272..f5ed79645 100644 --- a/shared/src/i18n/it/settings.ts +++ b/shared/src/i18n/it/settings.ts @@ -115,6 +115,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Ritardi dei voli', 'settings.notifyFlightLanded': 'Atterraggi', 'settings.notifyTripReminder': 'Promemoria di viaggio', + 'settings.notifyMailBookingAdded': 'Prenotazioni aggiunte da e-mail', + 'settings.notifyMailBookingPending': 'Prenotazioni inoltrate da controllare', + 'settings.notifyMailIngestPaused': 'Importazione e-mail in pausa', 'settings.notifyTodoDue': 'Attività in scadenza', 'settings.notifyVacayInvite': 'Inviti fusione Vacay', 'settings.notifyVacayShare': 'Condivisioni calendario Vacay', diff --git a/shared/src/i18n/ja/index.ts b/shared/src/i18n/ja/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/ja/index.ts +++ b/shared/src/i18n/ja/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/ja/mailAdmin.ts b/shared/src/i18n/ja/mailAdmin.ts new file mode 100644 index 000000000..f9d824f90 --- /dev/null +++ b/shared/src/i18n/ja/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': '予約メールの取り込み', + 'mailAdmin.hint': '転送された予約確認メールが、適切な旅行の予約になります。', + 'mailAdmin.enabled': '予約メールを取り込む', + 'mailAdmin.enabledHint': + 'このインストールの全員に転送用アドレスを発行します。オフの間は、どのアドレスもメールを受け付けません。', + 'mailAdmin.transport.label': 'メールの受信方法', + 'mailAdmin.transport.off': 'アップロードのみ', + 'mailAdmin.transport.imap': 'IMAPメールボックス', + 'mailAdmin.transport.webhook': 'Webhook', + 'mailAdmin.aiDaily': '1人1日あたりのAI読み取りメール数', + 'mailAdmin.aiDailyHint': 'この数を超えると、機械で読み取れる予約データのみ読み取ります。', + 'mailAdmin.template': 'アドレスのテンプレート', + 'mailAdmin.templateHint': '転送先アドレスです。各ユーザーのコードが入る場所に{token}を入れてください。', + 'mailAdmin.imap.host': 'サーバー', + 'mailAdmin.imap.port': 'ポート', + 'mailAdmin.imap.tls': '暗号化', + 'mailAdmin.imap.user': 'ユーザー', + 'mailAdmin.imap.pass': 'パスワード', + 'mailAdmin.imap.mailbox': 'フォルダ', + 'mailAdmin.imap.mailboxHint': '読み取り済みのメールはTREK/processedに移動します。', + 'mailAdmin.webhook.secret': '署名シークレット', + 'mailAdmin.webhook.secretHint': + '32文字以上。送信側がこの値ですべてのメールに署名します。空にするとWebhookはオフになります。', + 'mailAdmin.inboundUrl': 'Webhookのアドレス', + 'mailAdmin.inboundUrlHint': 'お使いのメールサービスが、各メールを加工せず署名付きでここに送信します。', + 'mailAdmin.copyUrl': 'アドレスをコピー', + 'mailAdmin.envHint': '環境変数(MAIL_INGEST_*)は、これらの項目より優先されます。', + 'mailAdmin.loadError': 'メール取り込みの設定を読み込めませんでした。', + 'mailAdmin.problem.secretShort': 'シークレットは32文字以上必要です。', + 'mailAdmin.problem.portInvalid': '1から65535までのポート番号を入力してください。', + 'mailAdmin.problem.templateToken': 'アドレスには、各ユーザーのコードの位置に{token}が必要です。', + 'mailAdmin.problem.dailyInvalid': '0から1000までの整数を入力してください。', + 'mailAdmin.status.title': 'ステータス', + 'mailAdmin.status.lastRead': '最終読み取り:{time}', + 'mailAdmin.status.never': 'なし', + 'mailAdmin.status.mails': 'メール、24時間', + 'mailAdmin.status.added': '自動追加', + 'mailAdmin.status.waiting': '確認待ち', + 'mailAdmin.status.failed': '失敗', + 'mailAdmin.test.run': '接続をテスト', + 'mailAdmin.test.ok': '接続できました', + 'mailAdmin.test.failed': '接続できません', +}; +export default mailAdmin; diff --git a/shared/src/i18n/ja/mailInbox.ts b/shared/src/i18n/ja/mailInbox.ts new file mode 100644 index 000000000..cd4f48ea6 --- /dev/null +++ b/shared/src/i18n/ja/mailInbox.ts @@ -0,0 +1,97 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': '確認待ちの予約', + 'mailInbox.pendingCount': '確認待ちの予約が{count}件あります', + 'mailInbox.empty': '確認待ちはありません', + 'mailInbox.emptyHint': 'TREKが自動で追加できなかった予約がここに表示され、それぞれの対処方法を選べます。', + 'mailInbox.untitled': '予約メール', + 'mailInbox.showMail': 'メールを表示', + 'mailInbox.backToList': '一覧に戻る', + 'mailInbox.loadMore': 'さらに表示', + 'mailInbox.pickTrip': 'どの旅行ですか?', + 'mailInbox.added': '予約を追加しました', + 'mailInbox.filed': '旅行のファイルに保存しました', + 'mailInbox.discarded': '破棄しました', + 'mailInbox.actionError': 'うまくいきませんでした。しばらくしてからもう一度お試しください。', + 'mailInbox.previewError': 'メールを表示できませんでした。', + 'mailInbox.action.add': '追加', + 'mailInbox.action.choose': '旅行に追加', + 'mailInbox.action.file': '書類として保存', + 'mailInbox.action.manual': '自分で入力', + 'mailInbox.action.discard': '破棄', + 'mailInbox.reason.forwardingConfirmation': + 'メールプロバイダが転送の確認を求めています。このコードをそちらに入力してください。', + 'mailInbox.reason.newsletter': 'ニュースレターで、予約ではありません', + 'mailInbox.reason.autoReply': '自動返信です', + 'mailInbox.reason.emptyMail': '読み取れる本文も添付ファイルもありません', + 'mailInbox.reason.mailUnreadable': 'メールを読み取れませんでした', + 'mailInbox.reason.notABooking': 'このメールに予約が見つかりません', + 'mailInbox.reason.tooLarge': 'メールが大きすぎて読み取れません', + 'mailInbox.reason.addressInvalid': 'すでに使えないアドレス宛てに届きました', + 'mailInbox.reason.addressDailyLimit': 'あなたのアドレスは今日の受信上限に達しました', + 'mailInbox.reason.processingFailed': '読み取りに何度も失敗しました', + 'mailInbox.reason.aiQuotaExhausted': '今期のAI利用枠を使い切りました', + 'mailInbox.reason.aiDisabled': 'AIなしで読み取りましたが、予約は見つかりませんでした', + 'mailInbox.reason.aiUserCap': 'AIで読み取るメールの1日の上限に達しました', + 'mailInbox.reason.aiProviderError': 'AIが今は読み取れませんでした', + 'mailInbox.reason.noTripInRange': 'この日付に当てはまる旅行がありません', + 'mailInbox.reason.tripAmbiguous': '該当しそうな旅行が複数あります', + 'mailInbox.reason.tripWithoutDates': '照合できる日付が旅行にありません', + 'mailInbox.reason.noEditPermission': 'その旅行に予約を追加する権限がありません', + 'mailInbox.reason.demoMode': 'デモでは予約を追加しません', + 'mailInbox.reason.outsideTripDates': '旅行の期間外の日付があります', + 'mailInbox.reason.sourceMismatch': 'メール内の情報が食い違っています', + 'mailInbox.reason.implausible': '正しくない可能性のある値があります', + 'mailInbox.reason.notInText': '読み取った値がメールに見当たりません', + 'mailInbox.reason.imageOnly': '画像のみから読み取ったため、確認が必要です', + 'mailInbox.reason.missingCriticalField': '種類、日付、またはルートがありません', + 'mailInbox.reason.missingConfirmation': '予約番号が見つかりません', + 'mailInbox.reason.tooManyBookings': '1通に含まれる予約が、自動で追加できる件数を超えています', + 'mailInbox.reason.changeOrCancellation': '以前の予約の変更またはキャンセルです', + 'mailInbox.reason.cancelled': 'この予約はキャンセルされています', + 'mailInbox.reason.possibleDuplicate': '旅行にすでにこの予約がある可能性があります', + 'mailInbox.reason.changeAvailable': '旅行にこの予約があり、このメールで内容が変更されます', + 'mailInbox.reason.conflict': '旅行にこの予約がありますが、内容が異なります', + 'mailInbox.reason.askFirst': '予約ごとに確認する設定になっています', + 'mailInbox.settings.title': '予約メール', + 'mailInbox.settings.on': 'オン', + 'mailInbox.settings.off': 'オフ', + 'mailInbox.settings.yourAddress': 'あなたのアドレス', + 'mailInbox.settings.copy': 'アドレスをコピー', + 'mailInbox.settings.copyError': 'コピーできませんでした', + 'mailInbox.settings.renew': '新しいアドレス', + 'mailInbox.settings.renewConfirm': 'もう一度タップしてください。古いアドレスは使えなくなります', + 'mailInbox.settings.renewed': '新しいアドレスができました', + 'mailInbox.settings.saveError': '保存できませんでした', + 'mailInbox.settings.addressHint': + '予約確認メールをこのアドレスに転送すると、TREKが適切な旅行に追加します。パスワードのように他の人には教えないでください。', + 'mailInbox.settings.howTo': '自動で転送する', + 'mailInbox.settings.howToGmail': + 'Gmail:設定、転送とPOP/IMAP、転送先アドレスを追加の順に開きます。Gmailからコードが届き、「確認待ちの予約」に表示されます。その後、予約の送信元からのメールをTREKに転送するフィルタを追加してください。', + 'mailInbox.settings.howToOutlook': + 'Outlook:設定、メール、ルールで、予約メールを転送するルールを追加します。職場のアカウントでは自動転送が禁止されている場合があり、その場合は手動で転送してください。', + 'mailInbox.settings.howToManual': '手動:予約メールを添付ファイルごと転送してください。', + 'mailInbox.settings.enabled': '予約メールを取り込む', + 'mailInbox.settings.enabledHint': 'オフにすると、あなたのアドレス宛てのメールは読み取られません。', + 'mailInbox.settings.ai': 'AIで読み取る', + 'mailInbox.settings.aiHint': + 'あらゆる予約メールを読み取り、該当する旅行を見つけます。メールと、旅行の日付や目的地がAIプロバイダに送信されます。', + 'mailInbox.settings.aiUnavailable': + 'ここでは利用できません。機械で読み取れる予約データを含むメールのみ読み取られます。', + 'mailInbox.settings.autoAdd': '自動で追加', + 'mailInbox.settings.autoAddHint': + 'TREKが確信できる予約はそのまま旅行に追加され、7日間は取り消せます。オフにすると、すべて確認を待ちます。', + 'mailInbox.settings.upload': 'メールファイルをアップロード', + 'mailInbox.settings.uploaded': 'メールを受信しました。読み取り中です', + 'mailInbox.settings.uploadDuplicate': 'このメールは取り込み済みです', + 'mailInbox.settings.uploadError': 'メールとして読み取れないファイルです', + 'mailInbox.settings.privacy': + '転送されたメールは暗号化して最長30日間保管され、その後削除されます。身分証明書や健康に関する書類は転送しないでください。', + 'mailInbox.trip.title': 'またはメールで転送', + 'mailInbox.trip.hint': 'この旅行のアドレスに送った予約メールは、推測なしでここに届きます。', + 'mailInbox.trip.create': 'この旅行用のアドレスを作成', + 'mailInbox.fromMail': 'メールから', + 'mailInbox.fromMailHint': '転送された予約メールから追加されました', +}; +export default mailInbox; diff --git a/shared/src/i18n/ja/notif.ts b/shared/src/i18n/ja/notif.ts index ef1083fa2..0dcfa7a70 100644 --- a/shared/src/i18n/ja/notif.ts +++ b/shared/src/i18n/ja/notif.ts @@ -58,7 +58,7 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} を {trip} に追加しました', 'notif.mail_booking_added.shared': '{actor} が転送メールから {booking} を {trip} に追加しました', 'notif.mail_booking_pending.title': '確認待ちの転送予約', - 'notif.mail_booking_pending.text': '確認待ちの転送予約: {count} 件', + 'notif.mail_booking_pending.text': '転送された予約が{count}件、確認待ちです', 'notif.mail_ingest_paused.title': 'メール取り込みを一時停止中', 'notif.mail_ingest_paused.text': '転送された予約メールは現在受け付けていません。アドレスはそのまま残り、取り込みが再開されると再び使えます。', diff --git a/shared/src/i18n/ja/reservations.ts b/shared/src/i18n/ja/reservations.ts index 584181929..4f7cee5a0 100644 --- a/shared/src/i18n/ja/reservations.ts +++ b/shared/src/i18n/ja/reservations.ts @@ -35,7 +35,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': '乗り継ぎ便', 'reservations.layover.layover': '乗り継ぎ', 'reservations.needsReview': '要確認', - 'reservations.needsReviewHint': '空港を自動で特定できませんでした。場所を確認してください。', + 'reservations.needsReviewHint': + '一部の項目は自動で入力されたもので、正確に読み取れていない可能性があります。ご確認ください。', + 'reservations.reviewConfirm': '問題なし', + 'reservations.reviewConfirmHint': 'クリックして確認済みにします。', + 'reservations.reviewConfirmed': '確認済みにしました', 'reservations.searchLocation': '駅・港・住所を検索…', 'reservations.meta.trainNumber': '列車番号', 'reservations.meta.platform': 'ホーム', diff --git a/shared/src/i18n/ja/settings.ts b/shared/src/i18n/ja/settings.ts index ec17fe270..88ada5786 100644 --- a/shared/src/i18n/ja/settings.ts +++ b/shared/src/i18n/ja/settings.ts @@ -113,6 +113,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'フライトの遅延', 'settings.notifyFlightLanded': 'フライトの着陸', 'settings.notifyTripReminder': '旅行リマインダー', + 'settings.notifyMailBookingAdded': 'メールから追加された予約', + 'settings.notifyMailBookingPending': '確認が必要な転送予約', + 'settings.notifyMailIngestPaused': 'メール取り込みの一時停止', 'settings.notifyTodoDue': 'ToDoの期限', 'settings.notifyVacayInvite': 'Vacay fusion の招待', 'settings.notifyVacayShare': 'Vacay カレンダーの共有', diff --git a/shared/src/i18n/ko/index.ts b/shared/src/i18n/ko/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/ko/index.ts +++ b/shared/src/i18n/ko/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/ko/mailAdmin.ts b/shared/src/i18n/ko/mailAdmin.ts new file mode 100644 index 000000000..6f97c7ca3 --- /dev/null +++ b/shared/src/i18n/ko/mailAdmin.ts @@ -0,0 +1,45 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': '예약 메일 가져오기', + 'mailAdmin.hint': '전달된 예약 확인 메일이 알맞은 여행의 예약이 됩니다.', + 'mailAdmin.enabled': '예약 메일 가져오기', + 'mailAdmin.enabledHint': '이 설치의 모든 사용자에게 전달용 주소를 제공합니다. 끄면 어떤 주소도 메일을 받지 않습니다.', + 'mailAdmin.transport.label': '메일 수신 방식', + 'mailAdmin.transport.off': '업로드만', + 'mailAdmin.transport.imap': 'IMAP 메일함', + 'mailAdmin.transport.webhook': '웹훅', + 'mailAdmin.aiDaily': '1인당 하루 AI 메일 수', + 'mailAdmin.aiDailyHint': '이 수를 넘으면 기계가 읽을 수 있는 예약 데이터만 읽습니다.', + 'mailAdmin.template': '주소 템플릿', + 'mailAdmin.templateHint': '전달 주소입니다. 사용자별 코드가 들어갈 자리에 {token}을 넣으세요.', + 'mailAdmin.imap.host': '서버', + 'mailAdmin.imap.port': '포트', + 'mailAdmin.imap.tls': '암호화', + 'mailAdmin.imap.user': '사용자', + 'mailAdmin.imap.pass': '비밀번호', + 'mailAdmin.imap.mailbox': '폴더', + 'mailAdmin.imap.mailboxHint': '읽은 메일은 TREK/processed로 이동합니다.', + 'mailAdmin.webhook.secret': '서명 시크릿', + 'mailAdmin.webhook.secretHint': '32자 이상. 발신 측이 이 값으로 모든 메일에 서명합니다. 비워 두면 웹훅이 꺼집니다.', + 'mailAdmin.inboundUrl': '웹훅 주소', + 'mailAdmin.inboundUrlHint': '메일 서비스가 각 메일을 원본 그대로 서명하여 이곳으로 보냅니다.', + 'mailAdmin.copyUrl': '주소 복사', + 'mailAdmin.envHint': '환경 변수(MAIL_INGEST_*)가 이 항목들보다 우선합니다.', + 'mailAdmin.loadError': '메일 가져오기 설정을 불러오지 못했습니다.', + 'mailAdmin.problem.secretShort': '시크릿은 32자 이상이어야 합니다.', + 'mailAdmin.problem.portInvalid': '1에서 65535 사이의 포트를 입력하세요.', + 'mailAdmin.problem.templateToken': '주소에는 사용자별 코드가 들어갈 자리에 {token}이 필요합니다.', + 'mailAdmin.problem.dailyInvalid': '0에서 1000 사이의 정수를 입력하세요.', + 'mailAdmin.status.title': '상태', + 'mailAdmin.status.lastRead': '마지막으로 읽음: {time}', + 'mailAdmin.status.never': '없음', + 'mailAdmin.status.mails': '메일, 24시간', + 'mailAdmin.status.added': '자동 추가', + 'mailAdmin.status.waiting': '대기 중', + 'mailAdmin.status.failed': '실패', + 'mailAdmin.test.run': '연결 테스트', + 'mailAdmin.test.ok': '연결됨', + 'mailAdmin.test.failed': '연결되지 않음', +}; +export default mailAdmin; diff --git a/shared/src/i18n/ko/mailInbox.ts b/shared/src/i18n/ko/mailInbox.ts new file mode 100644 index 000000000..310badb63 --- /dev/null +++ b/shared/src/i18n/ko/mailInbox.ts @@ -0,0 +1,98 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': '대기 중인 예약', + 'mailInbox.pendingCount': '확인을 기다리는 예약이 {count}건 있습니다', + 'mailInbox.empty': '대기 중인 항목이 없습니다', + 'mailInbox.emptyHint': + 'TREK이 자동으로 추가하지 못한 예약이 여기에 표시되며, 각각에 대해 할 수 있는 작업을 고를 수 있습니다.', + 'mailInbox.untitled': '예약 메일', + 'mailInbox.showMail': '메일 보기', + 'mailInbox.backToList': '목록으로 돌아가기', + 'mailInbox.loadMore': '더 보기', + 'mailInbox.pickTrip': '어느 여행인가요?', + 'mailInbox.added': '예약을 추가했습니다', + 'mailInbox.filed': '여행 파일에 저장했습니다', + 'mailInbox.discarded': '삭제했습니다', + 'mailInbox.actionError': '처리하지 못했습니다. 잠시 후 다시 시도하세요.', + 'mailInbox.previewError': '메일을 표시할 수 없습니다.', + 'mailInbox.action.add': '추가', + 'mailInbox.action.choose': '여행에 추가', + 'mailInbox.action.file': '문서로 저장', + 'mailInbox.action.manual': '직접 입력', + 'mailInbox.action.discard': '삭제', + 'mailInbox.reason.forwardingConfirmation': + '메일 제공업체가 전달 설정 확인을 요청하고 있습니다. 이 코드를 그곳에 입력하세요.', + 'mailInbox.reason.newsletter': '예약이 아닌 뉴스레터입니다', + 'mailInbox.reason.autoReply': '자동 답장입니다', + 'mailInbox.reason.emptyMail': '읽을 수 있는 본문이나 첨부 파일이 없습니다', + 'mailInbox.reason.mailUnreadable': '메일을 읽을 수 없었습니다', + 'mailInbox.reason.notABooking': '이 메일에서 예약을 찾지 못했습니다', + 'mailInbox.reason.tooLarge': '메일이 너무 커서 읽을 수 없습니다', + 'mailInbox.reason.addressInvalid': '더 이상 사용할 수 없는 주소로 도착했습니다', + 'mailInbox.reason.addressDailyLimit': '내 주소가 오늘 받을 수 있는 메일 수를 모두 채웠습니다', + 'mailInbox.reason.processingFailed': '읽기에 여러 번 실패했습니다', + 'mailInbox.reason.aiQuotaExhausted': '이번 기간의 AI 사용량을 모두 사용했습니다', + 'mailInbox.reason.aiDisabled': 'AI 없이 읽었지만 예약을 찾지 못했습니다', + 'mailInbox.reason.aiUserCap': 'AI로 읽는 메일의 하루 한도에 도달했습니다', + 'mailInbox.reason.aiProviderError': 'AI가 지금은 읽을 수 없었습니다', + 'mailInbox.reason.noTripInRange': '이 날짜에 해당하는 여행이 없습니다', + 'mailInbox.reason.tripAmbiguous': '해당할 수 있는 여행이 둘 이상입니다', + 'mailInbox.reason.tripWithoutDates': '대조할 날짜가 여행에 없습니다', + 'mailInbox.reason.noEditPermission': '해당 여행에 예약을 추가할 권한이 없습니다', + 'mailInbox.reason.demoMode': '데모에서는 예약을 추가하지 않습니다', + 'mailInbox.reason.outsideTripDates': '여행 기간을 벗어난 날짜가 있습니다', + 'mailInbox.reason.sourceMismatch': '메일 안의 정보가 서로 맞지 않습니다', + 'mailInbox.reason.implausible': '올바르지 않아 보이는 값이 있습니다', + 'mailInbox.reason.notInText': '읽어 낸 값이 메일에 없습니다', + 'mailInbox.reason.imageOnly': '이미지만으로 읽어 확인이 필요합니다', + 'mailInbox.reason.missingCriticalField': '유형, 날짜 또는 경로가 없습니다', + 'mailInbox.reason.missingConfirmation': '예약 번호를 찾지 못했습니다', + 'mailInbox.reason.tooManyBookings': '한 메일에 자동으로 추가할 수 있는 수보다 많은 예약이 들어 있습니다', + 'mailInbox.reason.changeOrCancellation': '이전 예약의 변경 또는 취소입니다', + 'mailInbox.reason.cancelled': '취소된 예약입니다', + 'mailInbox.reason.possibleDuplicate': '여행에 이미 이 예약이 있을 수 있습니다', + 'mailInbox.reason.changeAvailable': '여행에 이 예약이 있으며, 이 메일이 내용을 변경합니다', + 'mailInbox.reason.conflict': '여행에 이 예약이 있지만 내용이 다릅니다', + 'mailInbox.reason.askFirst': '예약마다 확인하도록 설정했습니다', + 'mailInbox.settings.title': '예약 메일', + 'mailInbox.settings.on': '켜짐', + 'mailInbox.settings.off': '꺼짐', + 'mailInbox.settings.yourAddress': '내 주소', + 'mailInbox.settings.copy': '주소 복사', + 'mailInbox.settings.copyError': '복사하지 못했습니다', + 'mailInbox.settings.renew': '새 주소', + 'mailInbox.settings.renewConfirm': '한 번 더 누르세요. 이전 주소는 사용할 수 없게 됩니다', + 'mailInbox.settings.renewed': '새 주소가 준비되었습니다', + 'mailInbox.settings.saveError': '저장하지 못했습니다', + 'mailInbox.settings.addressHint': + '예약 확인 메일을 이 주소로 전달하면 TREK이 알맞은 여행에 추가합니다. 비밀번호처럼 다른 사람에게 알려 주지 마세요.', + 'mailInbox.settings.howTo': '자동으로 전달하기', + 'mailInbox.settings.howToGmail': + 'Gmail: 설정, 전달 및 POP/IMAP, 전달 주소 추가 순서로 이동합니다. Gmail이 코드를 보내 주며, 이 코드는 대기 중인 예약에 표시됩니다. 그런 다음 예약 발신자의 메일을 TREK으로 전달하는 필터를 추가하세요.', + 'mailInbox.settings.howToOutlook': + 'Outlook: 설정, 메일, 규칙에서 예약 메일을 전달하는 규칙을 추가합니다. 회사 계정은 자동 전달이 막혀 있는 경우가 있으며, 이때는 직접 전달하세요.', + 'mailInbox.settings.howToManual': '직접 전달: 예약 메일을 첨부 파일까지 포함해 전달하세요.', + 'mailInbox.settings.enabled': '예약 메일 가져오기', + 'mailInbox.settings.enabledHint': '끄면 내 주소로 온 메일을 읽지 않습니다.', + 'mailInbox.settings.ai': 'AI로 읽기', + 'mailInbox.settings.aiHint': + '모든 예약 메일을 읽고 해당하는 여행을 찾습니다. 메일과 여행의 날짜 및 목적지가 AI 제공업체로 전송됩니다.', + 'mailInbox.settings.aiUnavailable': + '여기서는 사용할 수 없습니다. 기계가 읽을 수 있는 예약 데이터가 담긴 메일만 읽습니다.', + 'mailInbox.settings.autoAdd': '자동으로 추가', + 'mailInbox.settings.autoAddHint': + 'TREK이 확신하는 예약은 바로 여행에 추가되며, 7일 동안 되돌릴 수 있습니다. 끄면 모든 예약이 내 확인을 기다립니다.', + 'mailInbox.settings.upload': '메일 파일 업로드', + 'mailInbox.settings.uploaded': '메일을 받았습니다. 읽는 중입니다', + 'mailInbox.settings.uploadDuplicate': '이미 가져온 메일입니다', + 'mailInbox.settings.uploadError': '메일로 읽을 수 없는 파일입니다', + 'mailInbox.settings.privacy': + '전달된 메일은 암호화되어 최대 30일간 보관된 후 삭제됩니다. 신분증이나 건강 관련 문서는 전달하지 마세요.', + 'mailInbox.trip.title': '또는 메일로 전달', + 'mailInbox.trip.hint': '이 여행의 주소로 보낸 예약 메일은 추측 없이 바로 여기에 들어옵니다.', + 'mailInbox.trip.create': '이 여행용 주소 만들기', + 'mailInbox.fromMail': '메일에서', + 'mailInbox.fromMailHint': '전달된 예약 메일에서 추가되었습니다', +}; +export default mailInbox; diff --git a/shared/src/i18n/ko/notif.ts b/shared/src/i18n/ko/notif.ts index 405c47f16..8905c08a4 100644 --- a/shared/src/i18n/ko/notif.ts +++ b/shared/src/i18n/ko/notif.ts @@ -58,7 +58,7 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking}이(가) {trip}에 추가되었습니다', 'notif.mail_booking_added.shared': '{actor} 님이 전달된 메일에서 {booking}을(를) {trip}에 추가했습니다', 'notif.mail_booking_pending.title': '검토할 전달된 예약', - 'notif.mail_booking_pending.text': '검토를 기다리는 전달된 예약: {count}개', + 'notif.mail_booking_pending.text': '전달된 예약 {count}건이 확인을 기다리고 있습니다', 'notif.mail_ingest_paused.title': '메일 가져오기 일시 중지됨', 'notif.mail_ingest_paused.text': '전달된 예약 메일은 당분간 받지 않습니다. 주소는 그대로 유지되며 가져오기가 다시 시작되면 다시 작동합니다.', diff --git a/shared/src/i18n/ko/reservations.ts b/shared/src/i18n/ko/reservations.ts index f31d83bda..e2762d277 100644 --- a/shared/src/i18n/ko/reservations.ts +++ b/shared/src/i18n/ko/reservations.ts @@ -35,7 +35,10 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': '연결편', 'reservations.layover.layover': '경유 대기', 'reservations.needsReview': '검토 필요', - 'reservations.needsReviewHint': '공항이 자동으로 매칭되지 않았습니다 — 위치를 확인해 주세요.', + 'reservations.needsReviewHint': '일부 항목은 자동으로 채워졌으며 정확히 읽지 못했을 수 있습니다. 확인해 주세요.', + 'reservations.reviewConfirm': '맞아요', + 'reservations.reviewConfirmHint': '눌러서 확인 완료로 표시합니다.', + 'reservations.reviewConfirmed': '확인 완료로 표시했습니다', 'reservations.searchLocation': '역, 항구, 주소 검색…', 'reservations.meta.trainNumber': '열차 번호', 'reservations.meta.platform': '플랫폼', diff --git a/shared/src/i18n/ko/settings.ts b/shared/src/i18n/ko/settings.ts index f8a513c99..64db890ad 100644 --- a/shared/src/i18n/ko/settings.ts +++ b/shared/src/i18n/ko/settings.ts @@ -114,6 +114,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': '항공편 지연', 'settings.notifyFlightLanded': '항공편 착륙', 'settings.notifyTripReminder': '여행 리마인더', + 'settings.notifyMailBookingAdded': '메일에서 추가된 예약', + 'settings.notifyMailBookingPending': '검토할 전달 예약', + 'settings.notifyMailIngestPaused': '메일 가져오기 일시 중지', 'settings.notifyTodoDue': '할 일 마감 임박', 'settings.notifyVacayInvite': 'Vacay 퓨전 초대', 'settings.notifyVacayShare': 'Vacay 캘린더 공유', diff --git a/shared/src/i18n/nl/index.ts b/shared/src/i18n/nl/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/nl/index.ts +++ b/shared/src/i18n/nl/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/nl/mailAdmin.ts b/shared/src/i18n/nl/mailAdmin.ts new file mode 100644 index 000000000..e8a8b2145 --- /dev/null +++ b/shared/src/i18n/nl/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Import van boekingsmails', + 'mailAdmin.hint': 'Doorgestuurde boekingsbevestigingen worden boekingen in de juiste reis.', + 'mailAdmin.enabled': 'Boekingsmails importeren', + 'mailAdmin.enabledHint': + 'Geeft iedereen op deze installatie een doorstuuradres. Uit: geen enkel adres neemt mail aan.', + 'mailAdmin.transport.label': 'Hoe mail binnenkomt', + 'mailAdmin.transport.off': 'Alleen uploaden', + 'mailAdmin.transport.imap': 'IMAP-mailbox', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'AI-mails per persoon per dag', + 'mailAdmin.aiDailyHint': 'Daarboven worden alleen machineleesbare boekingsgegevens gelezen.', + 'mailAdmin.template': 'Adressjabloon', + 'mailAdmin.templateHint': 'Het doorstuuradres, met {token} op de plek van de code van elke persoon.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Poort', + 'mailAdmin.imap.tls': 'Versleuteling', + 'mailAdmin.imap.user': 'Gebruiker', + 'mailAdmin.imap.pass': 'Wachtwoord', + 'mailAdmin.imap.mailbox': 'Map', + 'mailAdmin.imap.mailboxHint': 'Gelezen mails worden verplaatst naar TREK/processed.', + 'mailAdmin.webhook.secret': 'Ondertekeningsgeheim', + 'mailAdmin.webhook.secretHint': + 'Minimaal 32 tekens; de afzender ondertekent elke mail hiermee. Leeg schakelt de webhook uit.', + 'mailAdmin.inboundUrl': 'Webhook-adres', + 'mailAdmin.inboundUrlHint': 'Je mailservice post hier elke mail naartoe, ruw en ondertekend.', + 'mailAdmin.copyUrl': 'Adres kopiëren', + 'mailAdmin.envHint': 'Omgevingsvariabelen (MAIL_INGEST_*) gaan voor op deze velden.', + 'mailAdmin.loadError': 'De instellingen voor mailimport konden niet worden geladen.', + 'mailAdmin.problem.secretShort': 'Het geheim moet minimaal 32 tekens hebben.', + 'mailAdmin.problem.portInvalid': 'Voer een poort in tussen 1 en 65535.', + 'mailAdmin.problem.templateToken': 'Het adres heeft {token} nodig voor de code van elke persoon.', + 'mailAdmin.problem.dailyInvalid': 'Voer een geheel getal van 0 tot 1000 in.', + 'mailAdmin.status.title': 'Status', // same-as-en + 'mailAdmin.status.lastRead': 'Laatst gelezen {time}', + 'mailAdmin.status.never': 'nooit', + 'mailAdmin.status.mails': 'Mails, 24 u', + 'mailAdmin.status.added': 'Zelf toegevoegd', + 'mailAdmin.status.waiting': 'In afwachting', + 'mailAdmin.status.failed': 'Mislukt', + 'mailAdmin.test.run': 'Verbinding testen', + 'mailAdmin.test.ok': 'Verbonden', + 'mailAdmin.test.failed': 'Geen verbinding', +}; +export default mailAdmin; diff --git a/shared/src/i18n/nl/mailInbox.ts b/shared/src/i18n/nl/mailInbox.ts new file mode 100644 index 000000000..7792053db --- /dev/null +++ b/shared/src/i18n/nl/mailInbox.ts @@ -0,0 +1,98 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Boekingen in afwachting', + 'mailInbox.pendingCount': '{count} boekingen wachten op je', + 'mailInbox.pendingCount.one': '{count} boeking wacht op je', + 'mailInbox.empty': 'Niets in afwachting', + 'mailInbox.emptyHint': 'Boekingen die TREK niet zelf kon toevoegen verschijnen hier, met wat je ermee kunt doen.', + 'mailInbox.untitled': 'Boekingsmail', + 'mailInbox.showMail': 'Mail tonen', + 'mailInbox.backToList': 'Terug naar de lijst', + 'mailInbox.loadMore': 'Meer tonen', + 'mailInbox.pickTrip': 'Welke reis?', + 'mailInbox.added': 'Boeking toegevoegd', + 'mailInbox.filed': 'Opgeslagen in de bestanden van de reis', + 'mailInbox.discarded': 'Verworpen', + 'mailInbox.actionError': 'Dat is niet gelukt. Probeer het zo opnieuw.', + 'mailInbox.previewError': 'De mail kon niet worden getoond.', + 'mailInbox.action.add': 'Toevoegen', + 'mailInbox.action.choose': 'Aan een reis toevoegen', + 'mailInbox.action.file': 'Opslaan als document', + 'mailInbox.action.manual': 'Zelf invullen', + 'mailInbox.action.discard': 'Verwerpen', + 'mailInbox.reason.forwardingConfirmation': + 'Je mailprovider vraagt om de doorstuurregel te bevestigen. Voer daar deze code in.', + 'mailInbox.reason.newsletter': 'Een nieuwsbrief, geen boeking', + 'mailInbox.reason.autoReply': 'Een automatisch antwoord', + 'mailInbox.reason.emptyMail': 'De mail bevat geen tekst of bijlage om te lezen', + 'mailInbox.reason.mailUnreadable': 'De mail kon niet worden gelezen', + 'mailInbox.reason.notABooking': 'Geen boeking gevonden in deze mail', + 'mailInbox.reason.tooLarge': 'De mail is te groot om te lezen', + 'mailInbox.reason.addressInvalid': 'Binnengekomen op een adres dat niet meer werkt', + 'mailInbox.reason.addressDailyLimit': 'Je adres heeft vandaag zijn maximum aan mails ontvangen', + 'mailInbox.reason.processingFailed': 'Lezen is meerdere keren mislukt', + 'mailInbox.reason.aiQuotaExhausted': 'Het AI-tegoed van deze periode is op', + 'mailInbox.reason.aiDisabled': 'Zonder AI gelezen, en daarmee is geen boeking gevonden', + 'mailInbox.reason.aiUserCap': 'Je dagelijkse limiet voor met AI gelezen mails is bereikt', + 'mailInbox.reason.aiProviderError': 'De AI kon de mail nu niet lezen', + 'mailInbox.reason.noTripInRange': 'Geen enkele reis dekt deze data', + 'mailInbox.reason.tripAmbiguous': 'Er kan meer dan één reis bedoeld zijn', + 'mailInbox.reason.tripWithoutDates': 'De reis heeft geen data om mee te vergelijken', + 'mailInbox.reason.noEditPermission': 'Je mag geen boekingen aan die reis toevoegen', + 'mailInbox.reason.demoMode': 'De demo voegt geen boekingen toe', + 'mailInbox.reason.outsideTripDates': 'Een datum valt buiten de reis', + 'mailInbox.reason.sourceMismatch': 'De gegevens in de mail komen niet met elkaar overeen', + 'mailInbox.reason.implausible': 'Een waarde kan niet kloppen', + 'mailInbox.reason.notInText': 'Een uitgelezen waarde staat niet in de mail', + 'mailInbox.reason.imageOnly': 'Alleen uit een afbeelding gelezen, dus kijk er zelf naar', + 'mailInbox.reason.missingCriticalField': 'Type, datum of route ontbreekt', + 'mailInbox.reason.missingConfirmation': 'Geen boekingsnummer gevonden', + 'mailInbox.reason.tooManyBookings': 'Meer boekingen in één mail dan er automatisch worden toegevoegd', + 'mailInbox.reason.changeOrCancellation': 'Een wijziging of annulering van een eerdere boeking', + 'mailInbox.reason.cancelled': 'De boeking is geannuleerd', + 'mailInbox.reason.possibleDuplicate': 'De reis bevat deze boeking mogelijk al', + 'mailInbox.reason.changeAvailable': 'De reis bevat deze boeking, en de mail wijzigt hem', + 'mailInbox.reason.conflict': 'De reis bevat deze boeking met andere gegevens', + 'mailInbox.reason.askFirst': 'Je wilde elke boeking eerst bevestigen', + 'mailInbox.settings.title': 'Boekingsmails', + 'mailInbox.settings.on': 'Aan', + 'mailInbox.settings.off': 'Uit', + 'mailInbox.settings.yourAddress': 'Jouw adres', + 'mailInbox.settings.copy': 'Adres kopiëren', + 'mailInbox.settings.copyError': 'Kopiëren is niet gelukt', + 'mailInbox.settings.renew': 'Nieuw adres', + 'mailInbox.settings.renewConfirm': 'Tik nogmaals: het oude adres werkt dan niet meer', + 'mailInbox.settings.renewed': 'Je nieuwe adres is klaar', + 'mailInbox.settings.saveError': 'Opslaan is niet gelukt', + 'mailInbox.settings.addressHint': + 'Stuur boekingsbevestigingen door naar dit adres en TREK voegt ze toe aan de juiste reis. Houd het geheim, zoals een wachtwoord.', + 'mailInbox.settings.howTo': 'Automatisch doorsturen', + 'mailInbox.settings.howToGmail': + 'Gmail: Instellingen, Doorsturen en POP/IMAP, Een doorstuuradres toevoegen. Gmail stuurt een code, die onder Boekingen in afwachting verschijnt. Voeg daarna een filter toe voor boekingsafzenders dat doorstuurt naar TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Instellingen, E-mail, Regels, voeg een regel toe die boekingsmails doorstuurt. Sommige werkaccounts blokkeren automatisch doorsturen; stuur daar handmatig door.', + 'mailInbox.settings.howToManual': 'Handmatig: stuur een boekingsmail door, inclusief bijlagen.', + 'mailInbox.settings.enabled': 'Boekingsmails importeren', + 'mailInbox.settings.enabledHint': 'Uit: mails naar je adres worden niet gelezen.', + 'mailInbox.settings.ai': 'Met AI lezen', + 'mailInbox.settings.aiHint': + 'Leest elke boekingsmail en zoekt de bijbehorende reis. De mail en de data en bestemmingen van je reizen gaan naar de AI-provider.', + 'mailInbox.settings.aiUnavailable': + 'Hier niet beschikbaar: alleen mails met machineleesbare boekingsgegevens worden gelezen.', + 'mailInbox.settings.autoAdd': 'Zelf toevoegen', + 'mailInbox.settings.autoAddHint': + 'Een boeking waar TREK zeker van is, komt direct in de reis en je kunt dit 7 dagen ongedaan maken. Uit: elke boeking wacht op jouw akkoord.', + 'mailInbox.settings.upload': 'Een mailbestand uploaden', + 'mailInbox.settings.uploaded': 'Mail ontvangen, wordt gelezen', + 'mailInbox.settings.uploadDuplicate': 'Deze mail is al binnen', + 'mailInbox.settings.uploadError': 'Het bestand kon niet als mail worden gelezen', + 'mailInbox.settings.privacy': + 'Doorgestuurde mails worden maximaal 30 dagen versleuteld bewaard en daarna verwijderd. Stuur geen identiteits- of gezondheidsdocumenten door.', + 'mailInbox.trip.title': 'Of stuur door per mail', + 'mailInbox.trip.hint': 'Een boekingsmail naar het adres van deze reis komt hier terecht, zonder gokwerk.', + 'mailInbox.trip.create': 'Een adres voor deze reis aanmaken', + 'mailInbox.fromMail': 'Uit mail', + 'mailInbox.fromMailHint': 'Toegevoegd uit een doorgestuurde boekingsmail', +}; +export default mailInbox; diff --git a/shared/src/i18n/nl/notif.ts b/shared/src/i18n/nl/notif.ts index 896a88a29..8104a4a9d 100644 --- a/shared/src/i18n/nl/notif.ts +++ b/shared/src/i18n/nl/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} is toegevoegd aan {trip}', 'notif.mail_booking_added.shared': '{actor} heeft {booking} uit een doorgestuurde mail aan {trip} toegevoegd', 'notif.mail_booking_pending.title': 'Doorgestuurde boekingen om te controleren', - 'notif.mail_booking_pending.text': 'Doorgestuurde boekingen wachten op je: {count}', + 'notif.mail_booking_pending.text': '{count} doorgestuurde boekingen wachten op je', + 'notif.mail_booking_pending.text.one': '{count} doorgestuurde boeking wacht op je', 'notif.mail_ingest_paused.title': 'Mailimport gepauzeerd', 'notif.mail_ingest_paused.text': 'Doorgestuurde boekingsmails worden voorlopig niet aangenomen. Je adressen blijven en werken weer zodra de import terug is.', diff --git a/shared/src/i18n/nl/reservations.ts b/shared/src/i18n/nl/reservations.ts index fbcf2323e..50dda7948 100644 --- a/shared/src/i18n/nl/reservations.ts +++ b/shared/src/i18n/nl/reservations.ts @@ -36,7 +36,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Aansluiting', 'reservations.layover.layover': 'Overstap', 'reservations.needsReview': 'Controleren', - 'reservations.needsReviewHint': 'Luchthaven kon niet automatisch worden herkend — bevestig de locatie.', + 'reservations.needsReviewHint': + 'Sommige gegevens zijn automatisch ingevuld en konden niet met zekerheid worden gelezen. Controleer ze even.', + 'reservations.reviewConfirm': 'Klopt', + 'reservations.reviewConfirmHint': 'Klik om als gecontroleerd te markeren.', + 'reservations.reviewConfirmed': 'Als gecontroleerd gemarkeerd', 'reservations.searchLocation': 'Station, haven, adres zoeken...', 'reservations.meta.trainNumber': 'Treinnr.', 'reservations.meta.platform': 'Perron', diff --git a/shared/src/i18n/nl/settings.ts b/shared/src/i18n/nl/settings.ts index 57a1a13f1..191e99810 100644 --- a/shared/src/i18n/nl/settings.ts +++ b/shared/src/i18n/nl/settings.ts @@ -115,6 +115,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Vluchtvertragingen', 'settings.notifyFlightLanded': 'Landingen', 'settings.notifyTripReminder': 'Reisherinneringen', + 'settings.notifyMailBookingAdded': 'Boekingen toegevoegd uit mails', + 'settings.notifyMailBookingPending': 'Doorgestuurde boekingen om te controleren', + 'settings.notifyMailIngestPaused': 'Mailimport gepauzeerd', 'settings.notifyTodoDue': 'Taak verloopt', 'settings.notifyVacayInvite': 'Vacay-fusieuitnodigingen', 'settings.notifyVacayShare': 'Gedeelde Vacay-kalenders', diff --git a/shared/src/i18n/pl/index.ts b/shared/src/i18n/pl/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/pl/index.ts +++ b/shared/src/i18n/pl/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/pl/mailAdmin.ts b/shared/src/i18n/pl/mailAdmin.ts new file mode 100644 index 000000000..9d078253e --- /dev/null +++ b/shared/src/i18n/pl/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Import wiadomości z rezerwacjami', + 'mailAdmin.hint': 'Przekazane potwierdzenia rezerwacji stają się rezerwacjami we właściwej podróży.', + 'mailAdmin.enabled': 'Importuj wiadomości z rezerwacjami', + 'mailAdmin.enabledHint': + 'Daje każdemu użytkownikowi tej instalacji adres do przekazywania. Po wyłączeniu żaden adres nie przyjmuje poczty.', + 'mailAdmin.transport.label': 'Jak docierają wiadomości', + 'mailAdmin.transport.off': 'Tylko wgrywanie', + 'mailAdmin.transport.imap': 'Skrzynka IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Wiadomości z AI na osobę dziennie', + 'mailAdmin.aiDailyHint': 'Powyżej tego limitu odczytywane są tylko dane rezerwacji czytelne dla maszyn.', + 'mailAdmin.template': 'Szablon adresu', + 'mailAdmin.templateHint': 'Adres do przekazywania, w którym {token} zastępuje kod każdego użytkownika.', + 'mailAdmin.imap.host': 'Serwer', + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Szyfrowanie', + 'mailAdmin.imap.user': 'Użytkownik', + 'mailAdmin.imap.pass': 'Hasło', + 'mailAdmin.imap.mailbox': 'Folder', // same-as-en + 'mailAdmin.imap.mailboxHint': 'Odczytane wiadomości trafiają do TREK/processed.', + 'mailAdmin.webhook.secret': 'Klucz podpisu', + 'mailAdmin.webhook.secretHint': + 'Co najmniej 32 znaki; nadawca podpisuje nim każdą wiadomość. Pusty klucz wyłącza webhook.', + 'mailAdmin.inboundUrl': 'Adres webhooka', + 'mailAdmin.inboundUrlHint': 'Twoja usługa pocztowa wysyła tu każdą wiadomość, w postaci surowej i podpisanej.', + 'mailAdmin.copyUrl': 'Kopiuj adres', + 'mailAdmin.envHint': 'Zmienne środowiskowe (MAIL_INGEST_*) mają pierwszeństwo przed tymi polami.', + 'mailAdmin.loadError': 'Nie udało się wczytać ustawień importu wiadomości.', + 'mailAdmin.problem.secretShort': 'Klucz musi mieć co najmniej 32 znaki.', + 'mailAdmin.problem.portInvalid': 'Wpisz port od 1 do 65535.', + 'mailAdmin.problem.templateToken': 'Adres musi zawierać {token} dla kodu każdego użytkownika.', + 'mailAdmin.problem.dailyInvalid': 'Wpisz liczbę całkowitą od 0 do 1000.', + 'mailAdmin.status.title': 'Status', // same-as-en + 'mailAdmin.status.lastRead': 'Ostatni odczyt {time}', + 'mailAdmin.status.never': 'nigdy', + 'mailAdmin.status.mails': 'Wiadomości, 24 h', + 'mailAdmin.status.added': 'Dodane samodzielnie', + 'mailAdmin.status.waiting': 'Oczekujące', + 'mailAdmin.status.failed': 'Nieudane', + 'mailAdmin.test.run': 'Testuj połączenie', + 'mailAdmin.test.ok': 'Połączono', + 'mailAdmin.test.failed': 'Brak połączenia', +}; +export default mailAdmin; diff --git a/shared/src/i18n/pl/mailInbox.ts b/shared/src/i18n/pl/mailInbox.ts new file mode 100644 index 000000000..23e37ec31 --- /dev/null +++ b/shared/src/i18n/pl/mailInbox.ts @@ -0,0 +1,101 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Rezerwacje do zatwierdzenia', + 'mailInbox.pendingCount': '{count} rezerwacji czeka na Ciebie', + 'mailInbox.pendingCount.one': '{count} rezerwacja czeka na Ciebie', + 'mailInbox.pendingCount.few': '{count} rezerwacje czekają na Ciebie', + 'mailInbox.pendingCount.many': '{count} rezerwacji czeka na Ciebie', + 'mailInbox.empty': 'Nic nie czeka', + 'mailInbox.emptyHint': + 'Rezerwacje, których TREK nie mógł dodać sam, pojawią się tutaj razem z opcjami, co można z nimi zrobić.', + 'mailInbox.untitled': 'Wiadomość z rezerwacją', + 'mailInbox.showMail': 'Pokaż wiadomość', + 'mailInbox.backToList': 'Wróć do listy', + 'mailInbox.loadMore': 'Pokaż więcej', + 'mailInbox.pickTrip': 'Którą podróż?', + 'mailInbox.added': 'Rezerwacja dodana', + 'mailInbox.filed': 'Zapisano w plikach podróży', + 'mailInbox.discarded': 'Odrzucono', + 'mailInbox.actionError': 'To się nie udało. Spróbuj ponownie za chwilę.', + 'mailInbox.previewError': 'Nie udało się wyświetlić wiadomości.', + 'mailInbox.action.add': 'Dodaj', + 'mailInbox.action.choose': 'Dodaj do podróży', + 'mailInbox.action.file': 'Zapisz jako dokument', + 'mailInbox.action.manual': 'Uzupełnij samodzielnie', + 'mailInbox.action.discard': 'Odrzuć', + 'mailInbox.reason.forwardingConfirmation': + 'Twój dostawca poczty prosi o potwierdzenie przekazywania. Wpisz tam ten kod.', + 'mailInbox.reason.newsletter': 'Newsletter, a nie rezerwacja', + 'mailInbox.reason.autoReply': 'Automatyczna odpowiedź', + 'mailInbox.reason.emptyMail': 'Wiadomość nie zawiera tekstu ani załącznika do odczytania', + 'mailInbox.reason.mailUnreadable': 'Nie udało się odczytać wiadomości', + 'mailInbox.reason.notABooking': 'Nie znaleziono rezerwacji w tej wiadomości', + 'mailInbox.reason.tooLarge': 'Wiadomość jest zbyt duża, aby ją odczytać', + 'mailInbox.reason.addressInvalid': 'Dotarła na adres, który już nie działa', + 'mailInbox.reason.addressDailyLimit': 'Twój adres przyjął dziś maksymalną liczbę wiadomości', + 'mailInbox.reason.processingFailed': 'Odczyt kilkakrotnie się nie udał', + 'mailInbox.reason.aiQuotaExhausted': 'Limit AI na ten okres został wykorzystany', + 'mailInbox.reason.aiDisabled': 'Odczytano bez AI, która nie znalazła rezerwacji', + 'mailInbox.reason.aiUserCap': 'Osiągnięto dzienny limit wiadomości odczytywanych przez AI', + 'mailInbox.reason.aiProviderError': 'AI nie mogła tego teraz odczytać', + 'mailInbox.reason.noTripInRange': 'Żadna podróż nie obejmuje tych dat', + 'mailInbox.reason.tripAmbiguous': 'Może chodzić o więcej niż jedną podróż', + 'mailInbox.reason.tripWithoutDates': 'Podróż nie ma dat, z którymi można by porównać', + 'mailInbox.reason.noEditPermission': 'Nie możesz dodawać rezerwacji do tej podróży', + 'mailInbox.reason.demoMode': 'Demo nie dodaje rezerwacji', + 'mailInbox.reason.outsideTripDates': 'Data wypada poza podróżą', + 'mailInbox.reason.sourceMismatch': 'Dane w wiadomości są ze sobą sprzeczne', + 'mailInbox.reason.implausible': 'Jakaś wartość nie może być prawidłowa', + 'mailInbox.reason.notInText': 'Odczytanej wartości nie ma w wiadomości', + 'mailInbox.reason.imageOnly': 'Odczytano tylko z obrazu, dlatego trzeba to sprawdzić', + 'mailInbox.reason.missingCriticalField': 'Brakuje typu, daty lub trasy', + 'mailInbox.reason.missingConfirmation': 'Nie znaleziono numeru rezerwacji', + 'mailInbox.reason.tooManyBookings': 'W jednej wiadomości jest więcej rezerwacji, niż dodaje się automatycznie', + 'mailInbox.reason.changeOrCancellation': 'Zmiana lub anulowanie wcześniejszej rezerwacji', + 'mailInbox.reason.cancelled': 'Rezerwacja została anulowana', + 'mailInbox.reason.possibleDuplicate': 'Podróż może już zawierać tę rezerwację', + 'mailInbox.reason.changeAvailable': 'Podróż zawiera tę rezerwację, a wiadomość ją zmienia', + 'mailInbox.reason.conflict': 'Podróż zawiera tę rezerwację z innymi danymi', + 'mailInbox.reason.askFirst': 'Chcesz potwierdzać każdą rezerwację', + 'mailInbox.settings.title': 'Wiadomości z rezerwacjami', + 'mailInbox.settings.on': 'Włączone', + 'mailInbox.settings.off': 'Wyłączone', + 'mailInbox.settings.yourAddress': 'Twój adres', + 'mailInbox.settings.copy': 'Kopiuj adres', + 'mailInbox.settings.copyError': 'Nie udało się skopiować', + 'mailInbox.settings.renew': 'Nowy adres', + 'mailInbox.settings.renewConfirm': 'Dotknij ponownie: stary adres przestanie działać', + 'mailInbox.settings.renewed': 'Twój nowy adres jest gotowy', + 'mailInbox.settings.saveError': 'Nie udało się zapisać', + 'mailInbox.settings.addressHint': + 'Przekazuj potwierdzenia rezerwacji na ten adres, a TREK doda je do właściwej podróży. Traktuj go jak hasło.', + 'mailInbox.settings.howTo': 'Przekazuj automatycznie', + 'mailInbox.settings.howToGmail': + 'Gmail: Ustawienia, Przekazywanie i POP/IMAP, Dodaj adres przekazywania. Gmail wyśle kod, który pojawi się w sekcji Rezerwacje do zatwierdzenia. Potem dodaj filtr dla nadawców rezerwacji, który przekazuje wiadomości do TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Ustawienia, Poczta, Reguły, dodaj regułę przekazującą wiadomości z rezerwacjami. Niektóre konta służbowe blokują automatyczne przekazywanie, tam przekazuj ręcznie.', + 'mailInbox.settings.howToManual': 'Ręcznie: przekaż dowolną wiadomość z rezerwacją, razem z załącznikami.', + 'mailInbox.settings.enabled': 'Importuj wiadomości z rezerwacjami', + 'mailInbox.settings.enabledHint': 'Po wyłączeniu wiadomości wysłane na Twój adres nie są odczytywane.', + 'mailInbox.settings.ai': 'Odczytuj za pomocą AI', + 'mailInbox.settings.aiHint': + 'Odczytuje każdą wiadomość z rezerwacją i znajduje dla niej podróż. Wiadomość oraz daty i cele Twoich podróży trafiają do dostawcy AI.', + 'mailInbox.settings.aiUnavailable': + 'Niedostępne tutaj: odczytywane są tylko wiadomości z danymi rezerwacji w formie czytelnej dla maszyn.', + 'mailInbox.settings.autoAdd': 'Dodawaj samodzielnie', + 'mailInbox.settings.autoAddHint': + 'Rezerwacja, której TREK jest pewien, trafia od razu do podróży i przez 7 dni możesz ją cofnąć. Po wyłączeniu każda czeka na Twoje potwierdzenie.', + 'mailInbox.settings.upload': 'Wgraj plik wiadomości', + 'mailInbox.settings.uploaded': 'Wiadomość odebrana, trwa jej odczyt', + 'mailInbox.settings.uploadDuplicate': 'Ta wiadomość już tu jest', + 'mailInbox.settings.uploadError': 'Nie udało się odczytać pliku jako wiadomości', + 'mailInbox.settings.privacy': + 'Przekazane wiadomości są przechowywane w formie zaszyfrowanej przez najwyżej 30 dni, a potem usuwane. Nie przekazuj proszę dokumentów tożsamości ani dokumentów zdrowotnych.', + 'mailInbox.trip.title': 'Albo przekaż mailem', + 'mailInbox.trip.hint': 'Wiadomość z rezerwacją wysłana na adres tej podróży trafia tutaj bez zgadywania.', + 'mailInbox.trip.create': 'Utwórz adres dla tej podróży', + 'mailInbox.fromMail': 'Z wiadomości', + 'mailInbox.fromMailHint': 'Dodano z przekazanej wiadomości z rezerwacją', +}; +export default mailInbox; diff --git a/shared/src/i18n/pl/notif.ts b/shared/src/i18n/pl/notif.ts index 277f45361..c576173c0 100644 --- a/shared/src/i18n/pl/notif.ts +++ b/shared/src/i18n/pl/notif.ts @@ -61,7 +61,10 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} dodano do {trip}', 'notif.mail_booking_added.shared': '{actor} dodał(a) {booking} do {trip} z przekazanej wiadomości', 'notif.mail_booking_pending.title': 'Przekazane rezerwacje do sprawdzenia', - 'notif.mail_booking_pending.text': 'Przekazane rezerwacje czekające na Ciebie: {count}', + 'notif.mail_booking_pending.text': '{count} przekazanych rezerwacji czeka na Ciebie', + 'notif.mail_booking_pending.text.one': '{count} przekazana rezerwacja czeka na Ciebie', + 'notif.mail_booking_pending.text.few': '{count} przekazane rezerwacje czekają na Ciebie', + 'notif.mail_booking_pending.text.many': '{count} przekazanych rezerwacji czeka na Ciebie', 'notif.mail_ingest_paused.title': 'Import e-maili wstrzymany', 'notif.mail_ingest_paused.text': 'Przekazane e-maile z rezerwacjami nie są na razie przyjmowane. Twoje adresy zostają i znów zadziałają, gdy import wróci.', diff --git a/shared/src/i18n/pl/reservations.ts b/shared/src/i18n/pl/reservations.ts index 9006acb38..0f8c6e5de 100644 --- a/shared/src/i18n/pl/reservations.ts +++ b/shared/src/i18n/pl/reservations.ts @@ -57,7 +57,11 @@ const reservations: TranslationStrings = { 'reservations.type.train': 'Pociąg', 'reservations.type.car': 'Samochód', 'reservations.needsReview': 'Sprawdź', - 'reservations.needsReviewHint': 'Nie udało się automatycznie dopasować lotniska — potwierdź lokalizację.', + 'reservations.needsReviewHint': + 'Niektóre dane zostały uzupełnione automatycznie i nie dało się ich odczytać z pewnością. Sprawdź je proszę.', + 'reservations.reviewConfirm': 'Wygląda dobrze', + 'reservations.reviewConfirmHint': 'Kliknij, aby oznaczyć jako sprawdzone.', + 'reservations.reviewConfirmed': 'Oznaczono jako sprawdzone', 'reservations.searchLocation': 'Szukaj stacji, portu, adresu...', 'reservations.type.cruise': 'Rejs', 'reservations.type.event': 'Wydarzenie', diff --git a/shared/src/i18n/pl/settings.ts b/shared/src/i18n/pl/settings.ts index 746e90303..1ead803f5 100644 --- a/shared/src/i18n/pl/settings.ts +++ b/shared/src/i18n/pl/settings.ts @@ -113,6 +113,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Opóźnienia lotów', 'settings.notifyFlightLanded': 'Lądowania', 'settings.notifyTripReminder': 'Przypomnienia o podróżach', + 'settings.notifyMailBookingAdded': 'Rezerwacje dodane z wiadomości', + 'settings.notifyMailBookingPending': 'Przekazane rezerwacje do sprawdzenia', + 'settings.notifyMailIngestPaused': 'Import wiadomości wstrzymany', 'settings.notifyTodoDue': 'Zadanie z terminem', 'settings.notifyVacayInvite': 'Zaproszenia do połączenia kalendarzy', 'settings.notifyVacayShare': 'Udostępnienia kalendarza Vacay', diff --git a/shared/src/i18n/ru/index.ts b/shared/src/i18n/ru/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/ru/index.ts +++ b/shared/src/i18n/ru/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/ru/mailAdmin.ts b/shared/src/i18n/ru/mailAdmin.ts new file mode 100644 index 000000000..b54523bff --- /dev/null +++ b/shared/src/i18n/ru/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Импорт писем с бронированиями', + 'mailAdmin.hint': 'Пересланные подтверждения бронирований становятся бронированиями в нужной поездке.', + 'mailAdmin.enabled': 'Импортировать письма с бронированиями', + 'mailAdmin.enabledHint': + 'Даёт каждому пользователю этой установки адрес для пересылки. Если выключено, ни один адрес не принимает почту.', + 'mailAdmin.transport.label': 'Как приходят письма', + 'mailAdmin.transport.off': 'Только загрузка', + 'mailAdmin.transport.imap': 'Почтовый ящик IMAP', + 'mailAdmin.transport.webhook': 'Webhook', + 'mailAdmin.aiDaily': 'Писем с ИИ на человека в день', + 'mailAdmin.aiDailyHint': 'Сверх этого лимита читаются только машиночитаемые данные о бронировании.', + 'mailAdmin.template': 'Шаблон адреса', + 'mailAdmin.templateHint': 'Адрес для пересылки, где {token} заменяется кодом каждого пользователя.', + 'mailAdmin.imap.host': 'Сервер', + 'mailAdmin.imap.port': 'Порт', + 'mailAdmin.imap.tls': 'Шифрование', + 'mailAdmin.imap.user': 'Пользователь', + 'mailAdmin.imap.pass': 'Пароль', + 'mailAdmin.imap.mailbox': 'Папка', + 'mailAdmin.imap.mailboxHint': 'Прочитанные письма перемещаются в TREK/processed.', + 'mailAdmin.webhook.secret': 'Ключ подписи', + 'mailAdmin.webhook.secretHint': + 'Не менее 32 символов; отправитель подписывает им каждое письмо. Пустой ключ отключает webhook.', + 'mailAdmin.inboundUrl': 'Адрес webhook', + 'mailAdmin.inboundUrlHint': 'Ваша почтовая служба отправляет сюда каждое письмо в исходном виде и с подписью.', + 'mailAdmin.copyUrl': 'Копировать адрес', + 'mailAdmin.envHint': 'Переменные окружения (MAIL_INGEST_*) имеют приоритет над этими полями.', + 'mailAdmin.loadError': 'Не удалось загрузить настройки импорта писем.', + 'mailAdmin.problem.secretShort': 'Ключ должен содержать не менее 32 символов.', + 'mailAdmin.problem.portInvalid': 'Введите порт от 1 до 65535.', + 'mailAdmin.problem.templateToken': 'Адрес должен содержать {token} для кода каждого пользователя.', + 'mailAdmin.problem.dailyInvalid': 'Введите целое число от 0 до 1000.', + 'mailAdmin.status.title': 'Статус', + 'mailAdmin.status.lastRead': 'Последнее чтение {time}', + 'mailAdmin.status.never': 'никогда', + 'mailAdmin.status.mails': 'Письма, 24 ч', + 'mailAdmin.status.added': 'Добавлено самостоятельно', + 'mailAdmin.status.waiting': 'Ожидают', + 'mailAdmin.status.failed': 'Ошибки', + 'mailAdmin.test.run': 'Проверить подключение', + 'mailAdmin.test.ok': 'Подключено', + 'mailAdmin.test.failed': 'Нет подключения', +}; +export default mailAdmin; diff --git a/shared/src/i18n/ru/mailInbox.ts b/shared/src/i18n/ru/mailInbox.ts new file mode 100644 index 000000000..0c823529b --- /dev/null +++ b/shared/src/i18n/ru/mailInbox.ts @@ -0,0 +1,100 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Бронирования на проверке', + 'mailInbox.pendingCount': '{count} бронирования ждёт вас', + 'mailInbox.pendingCount.one': '{count} бронирование ждёт вас', + 'mailInbox.pendingCount.few': '{count} бронирования ждут вас', + 'mailInbox.pendingCount.many': '{count} бронирований ждут вас', + 'mailInbox.empty': 'Ничего не ждёт', + 'mailInbox.emptyHint': + 'Бронирования, которые TREK не смог добавить сам, появятся здесь вместе с вариантами, что с ними можно сделать.', + 'mailInbox.untitled': 'Письмо с бронированием', + 'mailInbox.showMail': 'Показать письмо', + 'mailInbox.backToList': 'Назад к списку', + 'mailInbox.loadMore': 'Показать ещё', + 'mailInbox.pickTrip': 'Какая поездка?', + 'mailInbox.added': 'Бронирование добавлено', + 'mailInbox.filed': 'Сохранено в файлах поездки', + 'mailInbox.discarded': 'Отклонено', + 'mailInbox.actionError': 'Не получилось. Попробуйте ещё раз чуть позже.', + 'mailInbox.previewError': 'Не удалось показать письмо.', + 'mailInbox.action.add': 'Добавить', + 'mailInbox.action.choose': 'Добавить в поездку', + 'mailInbox.action.file': 'Сохранить как документ', + 'mailInbox.action.manual': 'Заполнить самостоятельно', + 'mailInbox.action.discard': 'Отклонить', + 'mailInbox.reason.forwardingConfirmation': 'Ваш почтовый сервис просит подтвердить пересылку. Введите там этот код.', + 'mailInbox.reason.newsletter': 'Рассылка, а не бронирование', + 'mailInbox.reason.autoReply': 'Автоматический ответ', + 'mailInbox.reason.emptyMail': 'В письме нет текста или вложения для чтения', + 'mailInbox.reason.mailUnreadable': 'Не удалось прочитать письмо', + 'mailInbox.reason.notABooking': 'В этом письме бронирование не найдено', + 'mailInbox.reason.tooLarge': 'Письмо слишком большое для чтения', + 'mailInbox.reason.addressInvalid': 'Оно пришло на адрес, который больше не работает', + 'mailInbox.reason.addressDailyLimit': 'Ваш адрес принял сегодня максимум писем', + 'mailInbox.reason.processingFailed': 'Прочитать письмо не удалось несколько раз', + 'mailInbox.reason.aiQuotaExhausted': 'Лимит ИИ на этот период исчерпан', + 'mailInbox.reason.aiDisabled': 'Прочитано без ИИ, бронирование не найдено', + 'mailInbox.reason.aiUserCap': 'Достигнут дневной лимит писем, читаемых ИИ', + 'mailInbox.reason.aiProviderError': 'ИИ сейчас не смог это прочитать', + 'mailInbox.reason.noTripInRange': 'Ни одна поездка не охватывает эти даты', + 'mailInbox.reason.tripAmbiguous': 'Подойти может более одной поездки', + 'mailInbox.reason.tripWithoutDates': 'У поездки нет дат для сверки', + 'mailInbox.reason.noEditPermission': 'Вам нельзя добавлять бронирования в эту поездку', + 'mailInbox.reason.demoMode': 'Демо не добавляет бронирования', + 'mailInbox.reason.outsideTripDates': 'Одна из дат выходит за рамки поездки', + 'mailInbox.reason.sourceMismatch': 'Данные письма противоречат друг другу', + 'mailInbox.reason.implausible': 'Одно из значений не может быть верным', + 'mailInbox.reason.notInText': 'Считанного значения нет в письме', + 'mailInbox.reason.imageOnly': 'Считано только с изображения, поэтому нужна ваша проверка', + 'mailInbox.reason.missingCriticalField': 'Не хватает типа, даты или маршрута', + 'mailInbox.reason.missingConfirmation': 'Номер бронирования не найден', + 'mailInbox.reason.tooManyBookings': 'В одном письме больше бронирований, чем добавляется автоматически', + 'mailInbox.reason.changeOrCancellation': 'Изменение или отмена прежнего бронирования', + 'mailInbox.reason.cancelled': 'Бронирование отменено', + 'mailInbox.reason.possibleDuplicate': 'Возможно, поездка уже содержит это бронирование', + 'mailInbox.reason.changeAvailable': 'Поездка содержит это бронирование, а письмо его изменяет', + 'mailInbox.reason.conflict': 'Поездка содержит это бронирование с другими данными', + 'mailInbox.reason.askFirst': 'Вы просили подтверждать каждое бронирование', + 'mailInbox.settings.title': 'Письма с бронированиями', + 'mailInbox.settings.on': 'Вкл.', + 'mailInbox.settings.off': 'Выкл.', + 'mailInbox.settings.yourAddress': 'Ваш адрес', + 'mailInbox.settings.copy': 'Копировать адрес', + 'mailInbox.settings.copyError': 'Не удалось скопировать', + 'mailInbox.settings.renew': 'Новый адрес', + 'mailInbox.settings.renewConfirm': 'Нажмите ещё раз: старый адрес перестанет работать', + 'mailInbox.settings.renewed': 'Ваш новый адрес готов', + 'mailInbox.settings.saveError': 'Не удалось сохранить', + 'mailInbox.settings.addressHint': + 'Пересылайте подтверждения бронирований на этот адрес, и TREK добавит их в нужную поездку. Храните его в тайне, как пароль.', + 'mailInbox.settings.howTo': 'Пересылать автоматически', + 'mailInbox.settings.howToGmail': + 'Gmail: Настройки, Пересылка и POP/IMAP, Добавить адрес пересылки. Gmail пришлёт код, который появится в разделе Бронирования на проверке. Затем добавьте фильтр для отправителей бронирований с пересылкой на TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Параметры, Почта, Правила, добавьте правило, которое пересылает письма с бронированиями. Некоторые рабочие аккаунты блокируют автоматическую пересылку, там пересылайте вручную.', + 'mailInbox.settings.howToManual': 'Вручную: перешлите любое письмо с бронированием вместе с вложениями.', + 'mailInbox.settings.enabled': 'Импортировать письма с бронированиями', + 'mailInbox.settings.enabledHint': 'Если выключено, письма на ваш адрес не читаются.', + 'mailInbox.settings.ai': 'Читать с помощью ИИ', + 'mailInbox.settings.aiHint': + 'Читает любое письмо с бронированием и находит для него поездку. Письмо, а также даты и направления ваших поездок передаются провайдеру ИИ.', + 'mailInbox.settings.aiUnavailable': + 'Здесь недоступно: читаются только письма с машиночитаемыми данными о бронировании.', + 'mailInbox.settings.autoAdd': 'Добавлять самостоятельно', + 'mailInbox.settings.autoAddHint': + 'Бронирование, в котором TREK уверен, сразу попадает в поездку, и 7 дней его можно отменить. Если выключено, каждое ждёт вашего подтверждения.', + 'mailInbox.settings.upload': 'Загрузить файл письма', + 'mailInbox.settings.uploaded': 'Письмо получено, оно обрабатывается', + 'mailInbox.settings.uploadDuplicate': 'Это письмо уже добавлено', + 'mailInbox.settings.uploadError': 'Не удалось прочитать файл как письмо', + 'mailInbox.settings.privacy': + 'Пересланные письма хранятся в зашифрованном виде не более 30 дней, затем удаляются. Пожалуйста, не пересылайте документы, удостоверяющие личность, и медицинские документы.', + 'mailInbox.trip.title': 'Или перешлите по почте', + 'mailInbox.trip.hint': 'Письмо с бронированием, отправленное на адрес этой поездки, попадает сюда без догадок.', + 'mailInbox.trip.create': 'Создать адрес для этой поездки', + 'mailInbox.fromMail': 'Из письма', + 'mailInbox.fromMailHint': 'Добавлено из пересланного письма с бронированием', +}; +export default mailInbox; diff --git a/shared/src/i18n/ru/notif.ts b/shared/src/i18n/ru/notif.ts index 440c16c22..1eba19e17 100644 --- a/shared/src/i18n/ru/notif.ts +++ b/shared/src/i18n/ru/notif.ts @@ -61,7 +61,10 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} добавлено в {trip}', 'notif.mail_booking_added.shared': '{actor} добавил(а) {booking} в {trip} из пересланного письма', 'notif.mail_booking_pending.title': 'Пересланные бронирования для проверки', - 'notif.mail_booking_pending.text': 'Пересланных бронирований ждёт вас: {count}', + 'notif.mail_booking_pending.text': '{count} пересланного бронирования ждёт вас', + 'notif.mail_booking_pending.text.one': '{count} пересланное бронирование ждёт вас', + 'notif.mail_booking_pending.text.few': '{count} пересланных бронирования ждут вас', + 'notif.mail_booking_pending.text.many': '{count} пересланных бронирований ждут вас', 'notif.mail_ingest_paused.title': 'Импорт писем приостановлен', 'notif.mail_ingest_paused.text': 'Пересланные письма с бронированиями пока не принимаются. Ваши адреса сохраняются и снова заработают, когда импорт вернётся.', diff --git a/shared/src/i18n/ru/reservations.ts b/shared/src/i18n/ru/reservations.ts index 67c4f1289..177a51f9f 100644 --- a/shared/src/i18n/ru/reservations.ts +++ b/shared/src/i18n/ru/reservations.ts @@ -35,7 +35,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Стыковка', 'reservations.layover.layover': 'Пересадка', 'reservations.needsReview': 'Проверить', - 'reservations.needsReviewHint': 'Аэропорт не удалось определить автоматически — подтвердите местоположение.', + 'reservations.needsReviewHint': + 'Часть данных заполнена автоматически и считана не со стопроцентной уверенностью. Пожалуйста, проверьте их.', + 'reservations.reviewConfirm': 'Всё верно', + 'reservations.reviewConfirmHint': 'Нажмите, чтобы отметить как проверенное.', + 'reservations.reviewConfirmed': 'Отмечено как проверенное', 'reservations.searchLocation': 'Искать станцию, порт, адрес...', 'reservations.meta.trainNumber': 'Номер поезда', 'reservations.meta.platform': 'Платформа', diff --git a/shared/src/i18n/ru/settings.ts b/shared/src/i18n/ru/settings.ts index 917431949..d74a14cd0 100644 --- a/shared/src/i18n/ru/settings.ts +++ b/shared/src/i18n/ru/settings.ts @@ -114,6 +114,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Задержки рейсов', 'settings.notifyFlightLanded': 'Посадки', 'settings.notifyTripReminder': 'Напоминания о поездке', + 'settings.notifyMailBookingAdded': 'Бронирования, добавленные из писем', + 'settings.notifyMailBookingPending': 'Пересланные бронирования для проверки', + 'settings.notifyMailIngestPaused': 'Импорт писем приостановлен', 'settings.notifyTodoDue': 'Задача к сроку', 'settings.notifyVacayInvite': 'Приглашения слияния Vacay', 'settings.notifyVacayShare': 'Общий доступ к календарю Vacay', diff --git a/shared/src/i18n/sk/index.ts b/shared/src/i18n/sk/index.ts index bb9ce471c..74f0d5974 100644 --- a/shared/src/i18n/sk/index.ts +++ b/shared/src/i18n/sk/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/sk/mailAdmin.ts b/shared/src/i18n/sk/mailAdmin.ts new file mode 100644 index 000000000..5ff198e22 --- /dev/null +++ b/shared/src/i18n/sk/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Import e-mailov s rezerváciami', + 'mailAdmin.hint': 'Preposlané potvrdenia rezervácií sa stanú rezerváciami v správnom výlete.', + 'mailAdmin.enabled': 'Importovať e-maily s rezerváciami', + 'mailAdmin.enabledHint': + 'Dá každému na tejto inštalácii adresu na preposielanie. Pri vypnutí žiadna adresa neprijíma poštu.', + 'mailAdmin.transport.label': 'Ako e-maily prichádzajú', + 'mailAdmin.transport.off': 'Iba nahrávanie', + 'mailAdmin.transport.imap': 'Schránka IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'E-maily s AI na osobu a deň', + 'mailAdmin.aiDailyHint': 'Nad tento limit sa čítajú len strojovo čitateľné údaje o rezervácii.', + 'mailAdmin.template': 'Šablóna adresy', + 'mailAdmin.templateHint': 'Adresa na preposielanie, kde {token} nahradí kód každého používateľa.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Šifrovanie', + 'mailAdmin.imap.user': 'Používateľ', + 'mailAdmin.imap.pass': 'Heslo', + 'mailAdmin.imap.mailbox': 'Priečinok', + 'mailAdmin.imap.mailboxHint': 'Prečítané e-maily sa presunú do TREK/processed.', + 'mailAdmin.webhook.secret': 'Podpisový kľúč', + 'mailAdmin.webhook.secretHint': + 'Aspoň 32 znakov; odosielateľ ním podpisuje každý e-mail. Prázdny kľúč webhook vypne.', + 'mailAdmin.inboundUrl': 'Adresa webhooku', + 'mailAdmin.inboundUrlHint': 'Tvoja e-mailová služba sem posiela každý e-mail, nespracovaný a podpísaný.', + 'mailAdmin.copyUrl': 'Kopírovať adresu', + 'mailAdmin.envHint': 'Premenné prostredia (MAIL_INGEST_*) majú prednosť pred týmito poľami.', + 'mailAdmin.loadError': 'Nastavenia importu e-mailov sa nepodarilo načítať.', + 'mailAdmin.problem.secretShort': 'Kľúč musí mať aspoň 32 znakov.', + 'mailAdmin.problem.portInvalid': 'Zadaj port medzi 1 a 65535.', + 'mailAdmin.problem.templateToken': 'Adresa musí obsahovať {token} pre kód každého používateľa.', + 'mailAdmin.problem.dailyInvalid': 'Zadaj celé číslo od 0 do 1000.', + 'mailAdmin.status.title': 'Stav', + 'mailAdmin.status.lastRead': 'Naposledy prečítané {time}', + 'mailAdmin.status.never': 'nikdy', + 'mailAdmin.status.mails': 'E-maily, 24 h', + 'mailAdmin.status.added': 'Pridané automaticky', + 'mailAdmin.status.waiting': 'Čaká', + 'mailAdmin.status.failed': 'Zlyhalo', + 'mailAdmin.test.run': 'Otestovať pripojenie', + 'mailAdmin.test.ok': 'Pripojené', + 'mailAdmin.test.failed': 'Bez pripojenia', +}; +export default mailAdmin; diff --git a/shared/src/i18n/sk/mailInbox.ts b/shared/src/i18n/sk/mailInbox.ts new file mode 100644 index 000000000..5e2b47cb6 --- /dev/null +++ b/shared/src/i18n/sk/mailInbox.ts @@ -0,0 +1,100 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Rezervácie čakajúce na schválenie', + 'mailInbox.pendingCount': '{count} rezervácií čaká na teba', + 'mailInbox.pendingCount.one': '{count} rezervácia čaká na teba', + 'mailInbox.pendingCount.few': '{count} rezervácie čakajú na teba', + 'mailInbox.empty': 'Nič nečaká', + 'mailInbox.emptyHint': + 'Rezervácie, ktoré TREK nedokázal pridať sám, sa zobrazia tu aj s možnosťami, čo s nimi urobiť.', + 'mailInbox.untitled': 'E-mail s rezerváciou', + 'mailInbox.showMail': 'Zobraziť e-mail', + 'mailInbox.backToList': 'Späť na zoznam', + 'mailInbox.loadMore': 'Zobraziť viac', + 'mailInbox.pickTrip': 'Ktorý výlet?', + 'mailInbox.added': 'Rezervácia pridaná', + 'mailInbox.filed': 'Uložené do súborov výletu', + 'mailInbox.discarded': 'Zahodené', + 'mailInbox.actionError': 'To sa nepodarilo. Skús to o chvíľu znova.', + 'mailInbox.previewError': 'E-mail sa nepodarilo zobraziť.', + 'mailInbox.action.add': 'Pridať', + 'mailInbox.action.choose': 'Pridať k výletu', + 'mailInbox.action.file': 'Uložiť ako dokument', + 'mailInbox.action.manual': 'Vyplniť ručne', + 'mailInbox.action.discard': 'Zahodiť', + 'mailInbox.reason.forwardingConfirmation': + 'Tvoj poskytovateľ e-mailu žiada o potvrdenie preposielania. Zadaj tam tento kód.', + 'mailInbox.reason.newsletter': 'Newsletter, nie rezervácia', + 'mailInbox.reason.autoReply': 'Automatická odpoveď', + 'mailInbox.reason.emptyMail': 'E-mail neobsahuje žiadny text ani prílohu na čítanie', + 'mailInbox.reason.mailUnreadable': 'E-mail sa nepodarilo prečítať', + 'mailInbox.reason.notABooking': 'V tomto e-maile sa nenašla žiadna rezervácia', + 'mailInbox.reason.tooLarge': 'E-mail je príliš veľký na čítanie', + 'mailInbox.reason.addressInvalid': 'Prišiel na adresu, ktorá už nefunguje', + 'mailInbox.reason.addressDailyLimit': 'Tvoja adresa dnes prijala maximum e-mailov', + 'mailInbox.reason.processingFailed': 'Čítanie zlyhalo viackrát', + 'mailInbox.reason.aiQuotaExhausted': 'Limit AI pre toto obdobie je vyčerpaný', + 'mailInbox.reason.aiDisabled': 'Prečítané bez AI, ktorá nenašla žiadnu rezerváciu', + 'mailInbox.reason.aiUserCap': 'Dosiahol si denný limit e-mailov čítaných pomocou AI', + 'mailInbox.reason.aiProviderError': 'AI to teraz nedokázala prečítať', + 'mailInbox.reason.noTripInRange': 'Žiadny výlet nepokrýva tieto dátumy', + 'mailInbox.reason.tripAmbiguous': 'Môže ísť o viac než jeden výlet', + 'mailInbox.reason.tripWithoutDates': 'Výlet nemá dátumy, s ktorými by sa dalo porovnať', + 'mailInbox.reason.noEditPermission': 'Do tohto výletu nesmieš pridávať rezervácie', + 'mailInbox.reason.demoMode': 'Demo nepridáva žiadne rezervácie', + 'mailInbox.reason.outsideTripDates': 'Dátum leží mimo výletu', + 'mailInbox.reason.sourceMismatch': 'Údaje v e-maile si navzájom odporujú', + 'mailInbox.reason.implausible': 'Niektorá hodnota nemôže byť správna', + 'mailInbox.reason.notInText': 'Načítaná hodnota v e-maile nie je', + 'mailInbox.reason.imageOnly': 'Prečítané len z obrázka, preto je potrebné, aby si sa na to pozrel', + 'mailInbox.reason.missingCriticalField': 'Chýba typ, dátum alebo trasa', + 'mailInbox.reason.missingConfirmation': 'Nenašlo sa číslo rezervácie', + 'mailInbox.reason.tooManyBookings': 'V jednom e-maile je viac rezervácií, než sa pridáva automaticky', + 'mailInbox.reason.changeOrCancellation': 'Zmena alebo zrušenie skoršej rezervácie', + 'mailInbox.reason.cancelled': 'Rezervácia je zrušená', + 'mailInbox.reason.possibleDuplicate': 'Výlet už túto rezerváciu možno obsahuje', + 'mailInbox.reason.changeAvailable': 'Výlet túto rezerváciu obsahuje a e-mail ju mení', + 'mailInbox.reason.conflict': 'Výlet obsahuje túto rezerváciu s inými údajmi', + 'mailInbox.reason.askFirst': 'Chcel si potvrdzovať každú rezerváciu', + 'mailInbox.settings.title': 'E-maily s rezerváciami', + 'mailInbox.settings.on': 'Zapnuté', + 'mailInbox.settings.off': 'Vypnuté', + 'mailInbox.settings.yourAddress': 'Tvoja adresa', + 'mailInbox.settings.copy': 'Kopírovať adresu', + 'mailInbox.settings.copyError': 'Nepodarilo sa skopírovať', + 'mailInbox.settings.renew': 'Nová adresa', + 'mailInbox.settings.renewConfirm': 'Klepni znova: stará adresa prestane fungovať', + 'mailInbox.settings.renewed': 'Tvoja nová adresa je pripravená', + 'mailInbox.settings.saveError': 'Nepodarilo sa uložiť', + 'mailInbox.settings.addressHint': + 'Preposielaj potvrdenia rezervácií na túto adresu a TREK ich pridá do správneho výletu. Chráň ju ako heslo.', + 'mailInbox.settings.howTo': 'Preposielať automaticky', + 'mailInbox.settings.howToGmail': + 'Gmail: Nastavenia, Preposielanie a POP/IMAP, Pridať adresu na preposielanie. Gmail pošle kód, ktorý sa zobrazí v časti Rezervácie čakajúce na schválenie. Potom pridaj filter pre odosielateľov rezervácií, ktorý preposiela na TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Nastavenia, Pošta, Pravidlá, pridaj pravidlo, ktoré preposiela e-maily s rezerváciami. Niektoré pracovné účty automatické preposielanie blokujú, tam preposielaj ručne.', + 'mailInbox.settings.howToManual': 'Ručne: prepošli akýkoľvek e-mail s rezerváciou, vrátane príloh.', + 'mailInbox.settings.enabled': 'Importovať e-maily s rezerváciami', + 'mailInbox.settings.enabledHint': 'Pri vypnutí sa e-maily na tvoju adresu nečítajú.', + 'mailInbox.settings.ai': 'Čítať pomocou AI', + 'mailInbox.settings.aiHint': + 'Prečíta akýkoľvek e-mail s rezerváciou a nájde k nemu výlet. E-mail a dátumy a ciele tvojich výletov sa odosielajú poskytovateľovi AI.', + 'mailInbox.settings.aiUnavailable': + 'Tu nie je dostupné: čítajú sa len e-maily so strojovo čitateľnými údajmi o rezervácii.', + 'mailInbox.settings.autoAdd': 'Pridávať automaticky', + 'mailInbox.settings.autoAddHint': + 'Rezervácia, v ktorej si je TREK istý, sa rovno pridá do výletu a 7 dní ju môžeš vrátiť späť. Pri vypnutí každá čaká na tvoje potvrdenie.', + 'mailInbox.settings.upload': 'Nahrať súbor e-mailu', + 'mailInbox.settings.uploaded': 'E-mail prijatý, práve sa číta', + 'mailInbox.settings.uploadDuplicate': 'Tento e-mail už tu je', + 'mailInbox.settings.uploadError': 'Súbor sa nepodarilo prečítať ako e-mail', + 'mailInbox.settings.privacy': + 'Preposlané e-maily sa uchovávajú zašifrované najviac 30 dní a potom sa zmažú. Nepreposielaj prosím doklady totožnosti ani zdravotné dokumenty.', + 'mailInbox.trip.title': 'Alebo prepošli e-mailom', + 'mailInbox.trip.hint': 'E-mail s rezerváciou poslaný na adresu tohto výletu skončí tu, bez hádania.', + 'mailInbox.trip.create': 'Vytvoriť adresu pre tento výlet', + 'mailInbox.fromMail': 'Z e-mailu', + 'mailInbox.fromMailHint': 'Pridané z preposlaného e-mailu s rezerváciou', +}; +export default mailInbox; diff --git a/shared/src/i18n/sk/notif.ts b/shared/src/i18n/sk/notif.ts index a8ec328a9..29fa78bcb 100644 --- a/shared/src/i18n/sk/notif.ts +++ b/shared/src/i18n/sk/notif.ts @@ -60,7 +60,9 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} bola pridaná do {trip}', 'notif.mail_booking_added.shared': '{actor} pridal(a) {booking} do {trip} z preposlaného e-mailu', 'notif.mail_booking_pending.title': 'Preposlané rezervácie na kontrolu', - 'notif.mail_booking_pending.text': 'Preposlané rezervácie čakajúce na vás: {count}', + 'notif.mail_booking_pending.text': '{count} preposlaných rezervácií čaká na vás', + 'notif.mail_booking_pending.text.one': '{count} preposlaná rezervácia čaká na vás', + 'notif.mail_booking_pending.text.few': '{count} preposlané rezervácie čakajú na vás', 'notif.mail_ingest_paused.title': 'Import e-mailov pozastavený', 'notif.mail_ingest_paused.text': 'Preposlané e-maily s rezerváciami sa zatiaľ neprijímajú. Vaše adresy zostávajú a znova budú fungovať, keď sa import vráti.', diff --git a/shared/src/i18n/sk/reservations.ts b/shared/src/i18n/sk/reservations.ts index 2119a7005..7ddb09b97 100644 --- a/shared/src/i18n/sk/reservations.ts +++ b/shared/src/i18n/sk/reservations.ts @@ -38,7 +38,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Prípoj', 'reservations.layover.layover': 'Medzipristátie', 'reservations.needsReview': 'Skontrolovať', - 'reservations.needsReviewHint': 'Letisko sa nepodarilo automaticky rozpoznať — potvrďte prosím miesto.', + 'reservations.needsReviewHint': + 'Niektoré údaje sa vyplnili automaticky a nedalo sa ich prečítať s istotou. Skontroluj ich prosím.', + 'reservations.reviewConfirm': 'Súhlasí', + 'reservations.reviewConfirmHint': 'Kliknutím označíš ako skontrolované.', + 'reservations.reviewConfirmed': 'Označené ako skontrolované', 'reservations.searchLocation': 'Hľadať stanicu, prístav, adresu…', 'reservations.meta.trainNumber': 'Číslo vlaku', 'reservations.meta.platform': 'Nástupište', diff --git a/shared/src/i18n/sk/settings.ts b/shared/src/i18n/sk/settings.ts index d595473ad..9786287a5 100644 --- a/shared/src/i18n/sk/settings.ts +++ b/shared/src/i18n/sk/settings.ts @@ -127,6 +127,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Meškania letov', 'settings.notifyFlightLanded': 'Pristátia', 'settings.notifyTripReminder': 'Pripomienky ciest', + 'settings.notifyMailBookingAdded': 'Rezervácie pridané z e-mailov', + 'settings.notifyMailBookingPending': 'Preposlané rezervácie na kontrolu', + 'settings.notifyMailIngestPaused': 'Import e-mailov pozastavený', 'settings.notifyTodoDue': 'Úloha sa blíži', 'settings.notifyVacayInvite': 'Pozvánky na prepojenie Vacay', 'settings.notifyVacayShare': 'Zdieľanie kalendára Vacay', diff --git a/shared/src/i18n/sv/index.ts b/shared/src/i18n/sv/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/sv/index.ts +++ b/shared/src/i18n/sv/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/sv/mailAdmin.ts b/shared/src/i18n/sv/mailAdmin.ts new file mode 100644 index 000000000..790a03324 --- /dev/null +++ b/shared/src/i18n/sv/mailAdmin.ts @@ -0,0 +1,46 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Import av bokningsmejl', + 'mailAdmin.hint': 'Vidarebefordrade bokningsbekräftelser blir bokningar i rätt resa.', + 'mailAdmin.enabled': 'Importera bokningsmejl', + 'mailAdmin.enabledHint': + 'Ger alla på den här installationen en vidarebefordringsadress. När det är av tar ingen adress emot mejl.', + 'mailAdmin.transport.label': 'Hur mejl kommer in', + 'mailAdmin.transport.off': 'Endast uppladdning', + 'mailAdmin.transport.imap': 'IMAP-brevlåda', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'AI-mejl per person och dag', + 'mailAdmin.aiDailyHint': 'Över gränsen läses endast maskinläsbara bokningsdata.', + 'mailAdmin.template': 'Adressmall', + 'mailAdmin.templateHint': 'Vidarebefordringsadressen, med {token} där varje persons kod ska stå.', + 'mailAdmin.imap.host': 'Server', // same-as-en + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Kryptering', + 'mailAdmin.imap.user': 'Användare', + 'mailAdmin.imap.pass': 'Lösenord', + 'mailAdmin.imap.mailbox': 'Mapp', + 'mailAdmin.imap.mailboxHint': 'Lästa mejl flyttas till TREK/processed.', + 'mailAdmin.webhook.secret': 'Signeringshemlighet', + 'mailAdmin.webhook.secretHint': 'Minst 32 tecken; avsändaren signerar varje mejl med den. Tom stänger av webhooken.', + 'mailAdmin.inboundUrl': 'Webhook-adress', + 'mailAdmin.inboundUrlHint': 'Din mejltjänst skickar varje mejl hit, rått och signerat.', + 'mailAdmin.copyUrl': 'Kopiera adress', + 'mailAdmin.envHint': 'Miljövariabler (MAIL_INGEST_*) gäller före de här fälten.', + 'mailAdmin.loadError': 'Inställningarna för mejlimport kunde inte laddas.', + 'mailAdmin.problem.secretShort': 'Hemligheten måste vara minst 32 tecken.', + 'mailAdmin.problem.portInvalid': 'Ange en port mellan 1 och 65535.', + 'mailAdmin.problem.templateToken': 'Adressen behöver {token} för varje persons kod.', + 'mailAdmin.problem.dailyInvalid': 'Ange ett heltal från 0 till 1000.', + 'mailAdmin.status.title': 'Status', // same-as-en + 'mailAdmin.status.lastRead': 'Senast läst {time}', + 'mailAdmin.status.never': 'aldrig', + 'mailAdmin.status.mails': 'Mejl, 24 h', + 'mailAdmin.status.added': 'Tillagda automatiskt', + 'mailAdmin.status.waiting': 'Väntar', + 'mailAdmin.status.failed': 'Misslyckade', + 'mailAdmin.test.run': 'Testa anslutning', + 'mailAdmin.test.ok': 'Ansluten', + 'mailAdmin.test.failed': 'Ingen anslutning', +}; +export default mailAdmin; diff --git a/shared/src/i18n/sv/mailInbox.ts b/shared/src/i18n/sv/mailInbox.ts new file mode 100644 index 000000000..d1b0becdf --- /dev/null +++ b/shared/src/i18n/sv/mailInbox.ts @@ -0,0 +1,98 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Väntande bokningar', + 'mailInbox.pendingCount': '{count} bokningar väntar på dig', + 'mailInbox.pendingCount.one': '{count} bokning väntar på dig', + 'mailInbox.empty': 'Inget väntar', + 'mailInbox.emptyHint': + 'Bokningar som TREK inte kunde lägga till själv visas här, med vad du kan göra med var och en.', + 'mailInbox.untitled': 'Bokningsmejl', + 'mailInbox.showMail': 'Visa mejl', + 'mailInbox.backToList': 'Tillbaka till listan', + 'mailInbox.loadMore': 'Visa fler', + 'mailInbox.pickTrip': 'Vilken resa?', + 'mailInbox.added': 'Bokningen lades till', + 'mailInbox.filed': 'Sparad i resans filer', + 'mailInbox.discarded': 'Kasserad', + 'mailInbox.actionError': 'Det gick inte. Försök igen om en stund.', + 'mailInbox.previewError': 'Mejlet kunde inte visas.', + 'mailInbox.action.add': 'Lägg till', + 'mailInbox.action.choose': 'Lägg till i en resa', + 'mailInbox.action.file': 'Spara som dokument', + 'mailInbox.action.manual': 'Fyll i själv', + 'mailInbox.action.discard': 'Kassera', + 'mailInbox.reason.forwardingConfirmation': + 'Din mejlleverantör ber om bekräftelse av vidarebefordran. Ange den här koden där.', + 'mailInbox.reason.newsletter': 'Ett nyhetsbrev, ingen bokning', + 'mailInbox.reason.autoReply': 'Ett automatiskt svar', + 'mailInbox.reason.emptyMail': 'Mejlet har ingen text eller bilaga att läsa', + 'mailInbox.reason.mailUnreadable': 'Mejlet kunde inte läsas', + 'mailInbox.reason.notABooking': 'Ingen bokning hittades i det här mejlet', + 'mailInbox.reason.tooLarge': 'Mejlet är för stort för att läsas', + 'mailInbox.reason.addressInvalid': 'Kom in på en adress som inte längre fungerar', + 'mailInbox.reason.addressDailyLimit': 'Din adress har tagit emot alla sina mejl för i dag', + 'mailInbox.reason.processingFailed': 'Det misslyckades flera gånger att läsa det', + 'mailInbox.reason.aiQuotaExhausted': 'AI-utrymmet för den här perioden är slut', + 'mailInbox.reason.aiDisabled': 'Läst utan AI, som inte hittade någon bokning', + 'mailInbox.reason.aiUserCap': 'Din dagliga gräns för AI-lästa mejl är nådd', + 'mailInbox.reason.aiProviderError': 'AI:n kunde inte läsa det just nu', + 'mailInbox.reason.noTripInRange': 'Ingen resa täcker de här datumen', + 'mailInbox.reason.tripAmbiguous': 'Det kan vara fler än en resa som avses', + 'mailInbox.reason.tripWithoutDates': 'Resan har inga datum att jämföra med', + 'mailInbox.reason.noEditPermission': 'Du får inte lägga till bokningar i den resan', + 'mailInbox.reason.demoMode': 'Demon lägger inte till några bokningar', + 'mailInbox.reason.outsideTripDates': 'Ett datum ligger utanför resan', + 'mailInbox.reason.sourceMismatch': 'Uppgifterna i mejlet motsäger varandra', + 'mailInbox.reason.implausible': 'Ett värde kan inte stämma', + 'mailInbox.reason.notInText': 'Ett avläst värde finns inte i mejlet', + 'mailInbox.reason.imageOnly': 'Läst enbart från en bild, så det behöver granskas av dig', + 'mailInbox.reason.missingCriticalField': 'Typ, datum eller rutt saknas', + 'mailInbox.reason.missingConfirmation': 'Ingen bokningsreferens hittades', + 'mailInbox.reason.tooManyBookings': 'Fler bokningar i ett mejl än vad som läggs till automatiskt', + 'mailInbox.reason.changeOrCancellation': 'En ändring eller avbokning av en tidigare bokning', + 'mailInbox.reason.cancelled': 'Bokningen är avbokad', + 'mailInbox.reason.possibleDuplicate': 'Resan kan redan innehålla den här bokningen', + 'mailInbox.reason.changeAvailable': 'Resan innehåller den här bokningen, och mejlet ändrar den', + 'mailInbox.reason.conflict': 'Resan innehåller den här bokningen med andra uppgifter', + 'mailInbox.reason.askFirst': 'Du bad om att få bekräfta varje bokning', + 'mailInbox.settings.title': 'Bokningsmejl', + 'mailInbox.settings.on': 'På', + 'mailInbox.settings.off': 'Av', + 'mailInbox.settings.yourAddress': 'Din adress', + 'mailInbox.settings.copy': 'Kopiera adress', + 'mailInbox.settings.copyError': 'Kunde inte kopiera', + 'mailInbox.settings.renew': 'Ny adress', + 'mailInbox.settings.renewConfirm': 'Tryck igen: den gamla adressen slutar fungera', + 'mailInbox.settings.renewed': 'Din nya adress är klar', + 'mailInbox.settings.saveError': 'Kunde inte spara', + 'mailInbox.settings.addressHint': + 'Vidarebefordra bokningsbekräftelser till den här adressen så lägger TREK till dem i rätt resa. Håll den för dig själv som ett lösenord.', + 'mailInbox.settings.howTo': 'Vidarebefordra automatiskt', + 'mailInbox.settings.howToGmail': + 'Gmail: Inställningar, Vidarebefordran och POP/IMAP, Lägg till en vidarebefordringsadress. Gmail skickar en kod, som visas under Väntande bokningar. Lägg sedan till ett filter för bokningsavsändare som vidarebefordrar till TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Inställningar, E-post, Regler, lägg till en regel som vidarebefordrar bokningsmejl. Vissa jobbkonton blockerar automatisk vidarebefordran; vidarebefordra för hand där.', + 'mailInbox.settings.howToManual': 'För hand: vidarebefordra valfritt bokningsmejl, bilagor inkluderade.', + 'mailInbox.settings.enabled': 'Importera bokningsmejl', + 'mailInbox.settings.enabledHint': 'När det är av läses inte mejl till din adress.', + 'mailInbox.settings.ai': 'Läs med AI', + 'mailInbox.settings.aiHint': + 'Läser alla bokningsmejl och hittar rätt resa. Mejlet samt datum och destinationer för dina resor skickas till AI-leverantören.', + 'mailInbox.settings.aiUnavailable': 'Inte tillgängligt här: endast mejl med maskinläsbara bokningsdata läses.', + 'mailInbox.settings.autoAdd': 'Lägg till automatiskt', + 'mailInbox.settings.autoAddHint': + 'En bokning som TREK är säker på hamnar direkt i sin resa, och du kan ångra det i 7 dagar. När det är av väntar varje bokning på ditt OK.', + 'mailInbox.settings.upload': 'Ladda upp en mejlfil', + 'mailInbox.settings.uploaded': 'Mejlet mottaget, det läses nu', + 'mailInbox.settings.uploadDuplicate': 'Det här mejlet finns redan', + 'mailInbox.settings.uploadError': 'Filen kunde inte läsas som ett mejl', + 'mailInbox.settings.privacy': + 'Vidarebefordrade mejl sparas krypterade i högst 30 dagar och raderas sedan. Vidarebefordra inte ID-handlingar eller hälsodokument.', + 'mailInbox.trip.title': 'Eller vidarebefordra med mejl', + 'mailInbox.trip.hint': 'Ett bokningsmejl som skickas till den här resans adress hamnar här utan gissningar.', + 'mailInbox.trip.create': 'Skapa en adress för den här resan', + 'mailInbox.fromMail': 'Från mejl', + 'mailInbox.fromMailHint': 'Tillagd från ett vidarebefordrat bokningsmejl', +}; +export default mailInbox; diff --git a/shared/src/i18n/sv/notif.ts b/shared/src/i18n/sv/notif.ts index b7775a3a6..fc7c4a714 100644 --- a/shared/src/i18n/sv/notif.ts +++ b/shared/src/i18n/sv/notif.ts @@ -59,7 +59,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} lades till i {trip}', 'notif.mail_booking_added.shared': '{actor} lade till {booking} i {trip} från ett vidarebefordrat mejl', 'notif.mail_booking_pending.title': 'Vidarebefordrade bokningar att granska', - 'notif.mail_booking_pending.text': 'Vidarebefordrade bokningar som väntar på dig: {count}', + 'notif.mail_booking_pending.text': '{count} vidarebefordrade bokningar väntar på dig', + 'notif.mail_booking_pending.text.one': '{count} vidarebefordrad bokning väntar på dig', 'notif.mail_ingest_paused.title': 'Mejlimport pausad', 'notif.mail_ingest_paused.text': 'Vidarebefordrade bokningsmejl tas inte emot just nu. Dina adresser finns kvar och fungerar igen när importen är tillbaka.', diff --git a/shared/src/i18n/sv/reservations.ts b/shared/src/i18n/sv/reservations.ts index f3a1ec81f..4b6fee1c1 100644 --- a/shared/src/i18n/sv/reservations.ts +++ b/shared/src/i18n/sv/reservations.ts @@ -36,7 +36,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Anslutning', 'reservations.layover.layover': 'Mellanlandning', 'reservations.needsReview': 'Granska', - 'reservations.needsReviewHint': 'Flygplatsen kunde inte matchas automatiskt – bekräfta platsen.', + 'reservations.needsReviewHint': + 'Vissa uppgifter fylldes i automatiskt och kunde inte läsas med säkerhet. Kontrollera dem.', + 'reservations.reviewConfirm': 'Ser rätt ut', + 'reservations.reviewConfirmHint': 'Klicka för att markera som kontrollerad.', + 'reservations.reviewConfirmed': 'Markerad som kontrollerad', 'reservations.searchLocation': 'Sök efter station, hamn, adress…', 'reservations.meta.trainNumber': 'Tågnummer', 'reservations.meta.platform': 'Plattform', diff --git a/shared/src/i18n/sv/settings.ts b/shared/src/i18n/sv/settings.ts index 8dbeca4a7..d061f40cf 100644 --- a/shared/src/i18n/sv/settings.ts +++ b/shared/src/i18n/sv/settings.ts @@ -120,6 +120,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Förseningar', 'settings.notifyFlightLanded': 'Landningar', 'settings.notifyTripReminder': 'Påminnelser inför resan', + 'settings.notifyMailBookingAdded': 'Bokningar tillagda från mejl', + 'settings.notifyMailBookingPending': 'Vidarebefordrade bokningar att granska', + 'settings.notifyMailIngestPaused': 'Mejlimporten pausad', 'settings.notifyTodoDue': 'Att göra ska göras snart', 'settings.notifyVacayInvite': 'Vacay samanslagnings inbjudningar', 'settings.notifyVacayShare': 'Vacay kalender delningar', diff --git a/shared/src/i18n/th/index.ts b/shared/src/i18n/th/index.ts index bb9ce471c..74f0d5974 100644 --- a/shared/src/i18n/th/index.ts +++ b/shared/src/i18n/th/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/th/mailAdmin.ts b/shared/src/i18n/th/mailAdmin.ts new file mode 100644 index 000000000..ff5e6a4d9 --- /dev/null +++ b/shared/src/i18n/th/mailAdmin.ts @@ -0,0 +1,46 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'การนำเข้าอีเมลการจอง', + 'mailAdmin.hint': 'อีเมลยืนยันการจองที่ส่งต่อมาจะกลายเป็นการจองในทริปที่ถูกต้อง', + 'mailAdmin.enabled': 'นำเข้าอีเมลการจอง', + 'mailAdmin.enabledHint': 'ให้ทุกคนในการติดตั้งนี้มีที่อยู่สำหรับส่งต่อ เมื่อปิด จะไม่มีที่อยู่ใดรับอีเมล', + 'mailAdmin.transport.label': 'วิธีรับอีเมล', + 'mailAdmin.transport.off': 'อัปโหลดเท่านั้น', + 'mailAdmin.transport.imap': 'กล่องจดหมาย IMAP', + 'mailAdmin.transport.webhook': 'เว็บฮุก', + 'mailAdmin.aiDaily': 'จำนวนอีเมล AI ต่อคนต่อวัน', + 'mailAdmin.aiDailyHint': 'เมื่อเกินจำนวนนี้ ระบบจะอ่านเฉพาะข้อมูลการจองที่เครื่องอ่านได้', + 'mailAdmin.template': 'รูปแบบที่อยู่', + 'mailAdmin.templateHint': 'ที่อยู่สำหรับส่งต่อ โดยใส่ {token} ตรงตำแหน่งรหัสของแต่ละคน', + 'mailAdmin.imap.host': 'เซิร์ฟเวอร์', + 'mailAdmin.imap.port': 'พอร์ต', + 'mailAdmin.imap.tls': 'การเข้ารหัส', + 'mailAdmin.imap.user': 'ผู้ใช้', + 'mailAdmin.imap.pass': 'รหัสผ่าน', + 'mailAdmin.imap.mailbox': 'โฟลเดอร์', + 'mailAdmin.imap.mailboxHint': 'อีเมลที่อ่านแล้วจะย้ายไปที่ TREK/processed', + 'mailAdmin.webhook.secret': 'รหัสลับสำหรับลงชื่อ', + 'mailAdmin.webhook.secretHint': + 'อย่างน้อย 32 ตัวอักษร ผู้ส่งจะใช้รหัสนี้ลงชื่อกำกับทุกอีเมล หากเว้นว่างไว้ เว็บฮุกจะปิดอยู่', + 'mailAdmin.inboundUrl': 'ที่อยู่เว็บฮุก', + 'mailAdmin.inboundUrlHint': 'บริการอีเมลของคุณจะส่งแต่ละอีเมลมาที่นี่ในรูปแบบดิบพร้อมลายเซ็น', + 'mailAdmin.copyUrl': 'คัดลอกที่อยู่', + 'mailAdmin.envHint': 'ตัวแปรสภาพแวดล้อม (MAIL_INGEST_*) มีผลเหนือกว่าช่องเหล่านี้', + 'mailAdmin.loadError': 'โหลดการตั้งค่าการนำเข้าอีเมลไม่ได้', + 'mailAdmin.problem.secretShort': 'รหัสลับต้องมีอย่างน้อย 32 ตัวอักษร', + 'mailAdmin.problem.portInvalid': 'กรอกพอร์ตระหว่าง 1 ถึง 65535', + 'mailAdmin.problem.templateToken': 'ที่อยู่ต้องมี {token} ตรงตำแหน่งรหัสของแต่ละคน', + 'mailAdmin.problem.dailyInvalid': 'กรอกจำนวนเต็มตั้งแต่ 0 ถึง 1000', + 'mailAdmin.status.title': 'สถานะ', + 'mailAdmin.status.lastRead': 'อ่านล่าสุด {time}', + 'mailAdmin.status.never': 'ไม่เคย', + 'mailAdmin.status.mails': 'อีเมล 24 ชม.', + 'mailAdmin.status.added': 'เพิ่มอัตโนมัติ', + 'mailAdmin.status.waiting': 'รออยู่', + 'mailAdmin.status.failed': 'ล้มเหลว', + 'mailAdmin.test.run': 'ทดสอบการเชื่อมต่อ', + 'mailAdmin.test.ok': 'เชื่อมต่อแล้ว', + 'mailAdmin.test.failed': 'เชื่อมต่อไม่ได้', +}; +export default mailAdmin; diff --git a/shared/src/i18n/th/mailInbox.ts b/shared/src/i18n/th/mailInbox.ts new file mode 100644 index 000000000..7278fd3ec --- /dev/null +++ b/shared/src/i18n/th/mailInbox.ts @@ -0,0 +1,95 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'การจองที่รอดำเนินการ', + 'mailInbox.pendingCount': 'มี {count} การจองรอให้คุณตรวจสอบ', + 'mailInbox.empty': 'ไม่มีรายการที่รออยู่', + 'mailInbox.emptyHint': 'การจองที่ TREK เพิ่มเองไม่ได้จะแสดงที่นี่ พร้อมตัวเลือกว่าคุณทำอะไรกับแต่ละรายการได้บ้าง', + 'mailInbox.untitled': 'อีเมลการจอง', + 'mailInbox.showMail': 'แสดงอีเมล', + 'mailInbox.backToList': 'กลับไปที่รายการ', + 'mailInbox.loadMore': 'แสดงเพิ่มเติม', + 'mailInbox.pickTrip': 'ทริปไหน?', + 'mailInbox.added': 'เพิ่มการจองแล้ว', + 'mailInbox.filed': 'บันทึกไว้ในไฟล์ของทริปแล้ว', + 'mailInbox.discarded': 'ทิ้งแล้ว', + 'mailInbox.actionError': 'ดำเนินการไม่สำเร็จ ลองอีกครั้งในอีกสักครู่', + 'mailInbox.previewError': 'แสดงอีเมลไม่ได้', + 'mailInbox.action.add': 'เพิ่ม', + 'mailInbox.action.choose': 'เพิ่มลงในทริป', + 'mailInbox.action.file': 'บันทึกเป็นเอกสาร', + 'mailInbox.action.manual': 'กรอกเอง', + 'mailInbox.action.discard': 'ทิ้ง', + 'mailInbox.reason.forwardingConfirmation': 'ผู้ให้บริการอีเมลของคุณขอให้ยืนยันการส่งต่อ กรอกรหัสนี้ที่นั่น', + 'mailInbox.reason.newsletter': 'เป็นจดหมายข่าว ไม่ใช่การจอง', + 'mailInbox.reason.autoReply': 'เป็นการตอบกลับอัตโนมัติ', + 'mailInbox.reason.emptyMail': 'อีเมลไม่มีข้อความหรือไฟล์แนบให้อ่าน', + 'mailInbox.reason.mailUnreadable': 'อ่านอีเมลไม่ได้', + 'mailInbox.reason.notABooking': 'ไม่พบการจองในอีเมลนี้', + 'mailInbox.reason.tooLarge': 'อีเมลมีขนาดใหญ่เกินกว่าจะอ่านได้', + 'mailInbox.reason.addressInvalid': 'ส่งมาที่ที่อยู่ซึ่งใช้งานไม่ได้แล้ว', + 'mailInbox.reason.addressDailyLimit': 'ที่อยู่ของคุณรับอีเมลครบตามจำนวนของวันนี้แล้ว', + 'mailInbox.reason.processingFailed': 'อ่านไม่สำเร็จหลายครั้ง', + 'mailInbox.reason.aiQuotaExhausted': 'โควตา AI ของรอบนี้ถูกใช้หมดแล้ว', + 'mailInbox.reason.aiDisabled': 'อ่านโดยไม่ใช้ AI และไม่พบการจอง', + 'mailInbox.reason.aiUserCap': 'ถึงขีดจำกัดรายวันของอีเมลที่อ่านด้วย AI แล้ว', + 'mailInbox.reason.aiProviderError': 'AI อ่านไม่ได้ในขณะนี้', + 'mailInbox.reason.noTripInRange': 'ไม่มีทริปที่ครอบคลุมวันที่เหล่านี้', + 'mailInbox.reason.tripAmbiguous': 'อาจหมายถึงทริปได้มากกว่าหนึ่งทริป', + 'mailInbox.reason.tripWithoutDates': 'ทริปไม่มีวันที่ให้ตรวจสอบเทียบ', + 'mailInbox.reason.noEditPermission': 'คุณไม่มีสิทธิ์เพิ่มการจองในทริปนั้น', + 'mailInbox.reason.demoMode': 'โหมดสาธิตไม่เพิ่มการจอง', + 'mailInbox.reason.outsideTripDates': 'มีวันที่อยู่นอกช่วงของทริป', + 'mailInbox.reason.sourceMismatch': 'รายละเอียดในอีเมลขัดแย้งกันเอง', + 'mailInbox.reason.implausible': 'มีค่าที่ไม่น่าจะถูกต้อง', + 'mailInbox.reason.notInText': 'ค่าที่อ่านได้ไม่มีอยู่ในอีเมล', + 'mailInbox.reason.imageOnly': 'อ่านจากรูปภาพเพียงอย่างเดียว จึงต้องให้คุณตรวจสอบ', + 'mailInbox.reason.missingCriticalField': 'ไม่มีประเภท วันที่ หรือเส้นทาง', + 'mailInbox.reason.missingConfirmation': 'ไม่พบหมายเลขการจอง', + 'mailInbox.reason.tooManyBookings': 'อีเมลฉบับเดียวมีการจองมากกว่าที่จะเพิ่มให้อัตโนมัติ', + 'mailInbox.reason.changeOrCancellation': 'การเปลี่ยนแปลงหรือยกเลิกการจองก่อนหน้า', + 'mailInbox.reason.cancelled': 'การจองนี้ถูกยกเลิกแล้ว', + 'mailInbox.reason.possibleDuplicate': 'ทริปอาจมีการจองนี้อยู่แล้ว', + 'mailInbox.reason.changeAvailable': 'ทริปมีการจองนี้อยู่แล้ว และอีเมลนี้เปลี่ยนแปลงรายละเอียด', + 'mailInbox.reason.conflict': 'ทริปมีการจองนี้อยู่แล้ว แต่รายละเอียดต่างกัน', + 'mailInbox.reason.askFirst': 'คุณตั้งค่าให้ยืนยันทุกการจอง', + 'mailInbox.settings.title': 'อีเมลการจอง', + 'mailInbox.settings.on': 'เปิด', + 'mailInbox.settings.off': 'ปิด', + 'mailInbox.settings.yourAddress': 'ที่อยู่ของคุณ', + 'mailInbox.settings.copy': 'คัดลอกที่อยู่', + 'mailInbox.settings.copyError': 'คัดลอกไม่ได้', + 'mailInbox.settings.renew': 'ที่อยู่ใหม่', + 'mailInbox.settings.renewConfirm': 'แตะอีกครั้ง ที่อยู่เดิมจะใช้งานไม่ได้อีก', + 'mailInbox.settings.renewed': 'ที่อยู่ใหม่ของคุณพร้อมแล้ว', + 'mailInbox.settings.saveError': 'บันทึกไม่ได้', + 'mailInbox.settings.addressHint': + 'ส่งต่ออีเมลยืนยันการจองมาที่ที่อยู่นี้ แล้ว TREK จะเพิ่มลงในทริปที่ถูกต้อง เก็บไว้เป็นความลับเหมือนรหัสผ่าน', + 'mailInbox.settings.howTo': 'ส่งต่ออัตโนมัติ', + 'mailInbox.settings.howToGmail': + 'Gmail: การตั้งค่า, การส่งต่อและ POP/IMAP, เพิ่มที่อยู่สำหรับส่งต่อ Gmail จะส่งรหัสมาให้ ซึ่งจะแสดงในส่วนการจองที่รอดำเนินการ จากนั้นเพิ่มตัวกรองสำหรับผู้ส่งอีเมลการจองให้ส่งต่อไปยัง TREK', + 'mailInbox.settings.howToOutlook': + 'Outlook: การตั้งค่า, อีเมล, กฎ แล้วเพิ่มกฎที่ส่งต่ออีเมลการจอง บัญชีของที่ทำงานบางบัญชีบล็อกการส่งต่ออัตโนมัติ ให้ส่งต่อด้วยตนเองในกรณีนั้น', + 'mailInbox.settings.howToManual': 'ด้วยตนเอง: ส่งต่ออีเมลการจองทุกฉบับ พร้อมไฟล์แนบ', + 'mailInbox.settings.enabled': 'นำเข้าอีเมลการจอง', + 'mailInbox.settings.enabledHint': 'เมื่อปิด อีเมลที่ส่งมาที่ที่อยู่ของคุณจะไม่ถูกอ่าน', + 'mailInbox.settings.ai': 'อ่านด้วย AI', + 'mailInbox.settings.aiHint': + 'อ่านอีเมลการจองทุกรูปแบบและหาทริปที่เกี่ยวข้อง อีเมลรวมถึงวันที่และจุดหมายของทริปจะถูกส่งไปยังผู้ให้บริการ AI', + 'mailInbox.settings.aiUnavailable': 'ใช้ไม่ได้ที่นี่ ระบบจะอ่านเฉพาะอีเมลที่มีข้อมูลการจองในรูปแบบที่เครื่องอ่านได้', + 'mailInbox.settings.autoAdd': 'เพิ่มอัตโนมัติ', + 'mailInbox.settings.autoAddHint': + 'การจองที่ TREK มั่นใจจะเข้าไปอยู่ในทริปทันที และคุณเลิกทำได้ภายใน 7 วัน เมื่อปิด ทุกรายการจะรอการยืนยันจากคุณ', + 'mailInbox.settings.upload': 'อัปโหลดไฟล์อีเมล', + 'mailInbox.settings.uploaded': 'ได้รับอีเมลแล้ว กำลังอ่านอยู่', + 'mailInbox.settings.uploadDuplicate': 'อีเมลนี้นำเข้าแล้ว', + 'mailInbox.settings.uploadError': 'อ่านไฟล์นี้เป็นอีเมลไม่ได้', + 'mailInbox.settings.privacy': + 'อีเมลที่ส่งต่อจะถูกเก็บไว้แบบเข้ารหัสไม่เกิน 30 วันแล้วลบทิ้ง โปรดอย่าส่งต่อเอกสารระบุตัวตนหรือเอกสารสุขภาพ', + 'mailInbox.trip.title': 'หรือส่งต่อทางอีเมล', + 'mailInbox.trip.hint': 'อีเมลการจองที่ส่งมาที่ที่อยู่ของทริปนี้จะมาอยู่ที่นี่โดยไม่ต้องเดา', + 'mailInbox.trip.create': 'สร้างที่อยู่สำหรับทริปนี้', + 'mailInbox.fromMail': 'จากอีเมล', + 'mailInbox.fromMailHint': 'เพิ่มจากอีเมลการจองที่ส่งต่อมา', +}; +export default mailInbox; diff --git a/shared/src/i18n/th/notif.ts b/shared/src/i18n/th/notif.ts index 0a01262e9..6291cfa91 100644 --- a/shared/src/i18n/th/notif.ts +++ b/shared/src/i18n/th/notif.ts @@ -58,7 +58,7 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': 'เพิ่ม {booking} ลงใน {trip} แล้ว', 'notif.mail_booking_added.shared': '{actor} เพิ่ม {booking} ลงใน {trip} จากอีเมลที่ส่งต่อ', 'notif.mail_booking_pending.title': 'การจองที่ส่งต่อซึ่งรอตรวจสอบ', - 'notif.mail_booking_pending.text': 'การจองที่ส่งต่อซึ่งรอคุณอยู่: {count}', + 'notif.mail_booking_pending.text': 'มี {count} การจองที่ส่งต่อมารอให้คุณตรวจสอบ', 'notif.mail_ingest_paused.title': 'หยุดการนำเข้าอีเมลชั่วคราว', 'notif.mail_ingest_paused.text': 'ขณะนี้ยังไม่รับอีเมลการจองที่ส่งต่อมา ที่อยู่ของคุณยังคงอยู่และจะใช้งานได้อีกครั้งเมื่อการนำเข้ากลับมา', diff --git a/shared/src/i18n/th/reservations.ts b/shared/src/i18n/th/reservations.ts index a6a8d393a..cf1cc69fa 100644 --- a/shared/src/i18n/th/reservations.ts +++ b/shared/src/i18n/th/reservations.ts @@ -37,7 +37,10 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'การเชื่อมต่อ', 'reservations.layover.layover': 'แวะพัก', 'reservations.needsReview': 'ตรวจสอบ', - 'reservations.needsReviewHint': 'ไม่สามารถจับคู่สนามบินได้โดยอัตโนมัติ — โปรดยืนยันตำแหน่ง', + 'reservations.needsReviewHint': 'รายละเอียดบางส่วนถูกกรอกอัตโนมัติและอาจอ่านได้ไม่แน่ชัด โปรดตรวจสอบ', + 'reservations.reviewConfirm': 'ถูกต้อง', + 'reservations.reviewConfirmHint': 'คลิกเพื่อทำเครื่องหมายว่าตรวจแล้ว', + 'reservations.reviewConfirmed': 'ทำเครื่องหมายว่าตรวจแล้ว', 'reservations.searchLocation': 'ค้นหาสถานี ท่าเรือ หรือที่อยู่…', 'reservations.meta.trainNumber': 'หมายเลขรถไฟ', 'reservations.meta.platform': 'แพลตฟอร์ม', diff --git a/shared/src/i18n/th/settings.ts b/shared/src/i18n/th/settings.ts index 670499379..6210c001f 100644 --- a/shared/src/i18n/th/settings.ts +++ b/shared/src/i18n/th/settings.ts @@ -112,6 +112,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'เที่ยวบินล่าช้า', 'settings.notifyFlightLanded': 'เที่ยวบินลงจอด', 'settings.notifyTripReminder': 'การแจ้งเตือนการเดินทาง', + 'settings.notifyMailBookingAdded': 'การจองที่เพิ่มจากอีเมล', + 'settings.notifyMailBookingPending': 'การจองที่ส่งต่อมาและรอตรวจสอบ', + 'settings.notifyMailIngestPaused': 'การนำเข้าอีเมลหยุดชั่วคราว', 'settings.notifyTodoDue': 'สิ่งที่ต้องทำจะครบกำหนดเร็วๆ นี้', 'settings.notifyVacayInvite': 'คำเชิญรวมปฏิทินวันลา', 'settings.notifyVacayShare': 'การแชร์ปฏิทินวันหยุด', diff --git a/shared/src/i18n/tr/index.ts b/shared/src/i18n/tr/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/tr/index.ts +++ b/shared/src/i18n/tr/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/tr/mailAdmin.ts b/shared/src/i18n/tr/mailAdmin.ts new file mode 100644 index 000000000..7ade77219 --- /dev/null +++ b/shared/src/i18n/tr/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Rezervasyon e-postası içe aktarma', + 'mailAdmin.hint': 'Yönlendirilen rezervasyon onayları doğru seyahatte rezervasyona dönüşür.', + 'mailAdmin.enabled': 'Rezervasyon e-postalarını içe aktar', + 'mailAdmin.enabledHint': + 'Bu kurulumdaki herkese bir yönlendirme adresi verir. Kapalıyken hiçbir adres e-posta kabul etmez.', + 'mailAdmin.transport.label': 'E-posta nasıl ulaşır', + 'mailAdmin.transport.off': 'Yalnızca yükleme', + 'mailAdmin.transport.imap': 'IMAP posta kutusu', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Kişi başı günlük yapay zeka e-postası', + 'mailAdmin.aiDailyHint': 'Bunun üstünde yalnızca makine tarafından okunabilir rezervasyon verisi okunur.', + 'mailAdmin.template': 'Adres şablonu', + 'mailAdmin.templateHint': 'Yönlendirme adresi; her kişinin kodunun geleceği yerde {token} bulunur.', + 'mailAdmin.imap.host': 'Sunucu', + 'mailAdmin.imap.port': 'Port', // same-as-en + 'mailAdmin.imap.tls': 'Şifreleme', + 'mailAdmin.imap.user': 'Kullanıcı', + 'mailAdmin.imap.pass': 'Parola', + 'mailAdmin.imap.mailbox': 'Klasör', + 'mailAdmin.imap.mailboxHint': 'Okunan e-postalar TREK/processed klasörüne taşınır.', + 'mailAdmin.webhook.secret': 'İmza anahtarı', + 'mailAdmin.webhook.secretHint': + 'En az 32 karakter; gönderen her e-postayı bununla imzalar. Boş bırakılırsa webhook kapanır.', + 'mailAdmin.inboundUrl': 'Webhook adresi', + 'mailAdmin.inboundUrlHint': 'E-posta servisin her e-postayı ham ve imzalı olarak buraya gönderir.', + 'mailAdmin.copyUrl': 'Adresi kopyala', + 'mailAdmin.envHint': 'Ortam değişkenleri (MAIL_INGEST_*) bu alanlardan önce gelir.', + 'mailAdmin.loadError': 'E-posta içe aktarma ayarları yüklenemedi.', + 'mailAdmin.problem.secretShort': 'Anahtar en az 32 karakter olmalı.', + 'mailAdmin.problem.portInvalid': '1 ile 65535 arasında bir port gir.', + 'mailAdmin.problem.templateToken': 'Adreste her kişinin kodu için {token} bulunmalı.', + 'mailAdmin.problem.dailyInvalid': '0 ile 1000 arasında bir tam sayı gir.', + 'mailAdmin.status.title': 'Durum', + 'mailAdmin.status.lastRead': 'Son okuma {time}', + 'mailAdmin.status.never': 'hiç', + 'mailAdmin.status.mails': 'E-postalar, 24 sa', + 'mailAdmin.status.added': 'Kendiliğinden eklenen', + 'mailAdmin.status.waiting': 'Bekleyen', + 'mailAdmin.status.failed': 'Başarısız', + 'mailAdmin.test.run': 'Bağlantıyı test et', + 'mailAdmin.test.ok': 'Bağlandı', + 'mailAdmin.test.failed': 'Bağlantı yok', +}; +export default mailAdmin; diff --git a/shared/src/i18n/tr/mailInbox.ts b/shared/src/i18n/tr/mailInbox.ts new file mode 100644 index 000000000..f265a01a9 --- /dev/null +++ b/shared/src/i18n/tr/mailInbox.ts @@ -0,0 +1,98 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Bekleyen rezervasyonlar', + 'mailInbox.pendingCount': '{count} rezervasyon seni bekliyor', + 'mailInbox.pendingCount.one': '{count} rezervasyon seni bekliyor', + 'mailInbox.empty': 'Bekleyen bir şey yok', + 'mailInbox.emptyHint': + 'TREK’in kendi başına ekleyemediği rezervasyonlar burada görünür, her biri için yapabileceklerinle birlikte.', + 'mailInbox.untitled': 'Rezervasyon e-postası', + 'mailInbox.showMail': 'E-postayı göster', + 'mailInbox.backToList': 'Listeye dön', + 'mailInbox.loadMore': 'Daha fazla göster', + 'mailInbox.pickTrip': 'Hangi seyahat?', + 'mailInbox.added': 'Rezervasyon eklendi', + 'mailInbox.filed': 'Seyahatin dosyalarına kaydedildi', + 'mailInbox.discarded': 'Silindi', + 'mailInbox.actionError': 'İşlem başarısız oldu. Biraz sonra tekrar dene.', + 'mailInbox.previewError': 'E-posta gösterilemedi.', + 'mailInbox.action.add': 'Ekle', + 'mailInbox.action.choose': 'Bir seyahate ekle', + 'mailInbox.action.file': 'Belge olarak kaydet', + 'mailInbox.action.manual': 'Kendin doldur', + 'mailInbox.action.discard': 'Sil', + 'mailInbox.reason.forwardingConfirmation': 'E-posta sağlayıcın yönlendirmeyi onaylamanı istiyor. Bu kodu orada gir.', + 'mailInbox.reason.newsletter': 'Bir bülten, rezervasyon değil', + 'mailInbox.reason.autoReply': 'Otomatik bir yanıt', + 'mailInbox.reason.emptyMail': 'E-postada okunacak metin veya ek yok', + 'mailInbox.reason.mailUnreadable': 'E-posta okunamadı', + 'mailInbox.reason.notABooking': 'Bu e-postada rezervasyon bulunamadı', + 'mailInbox.reason.tooLarge': 'E-posta okunamayacak kadar büyük', + 'mailInbox.reason.addressInvalid': 'Artık çalışmayan bir adrese geldi', + 'mailInbox.reason.addressDailyLimit': 'Adresin bugünkü e-posta sınırına ulaştı', + 'mailInbox.reason.processingFailed': 'Okuma birkaç kez başarısız oldu', + 'mailInbox.reason.aiQuotaExhausted': 'Bu dönemin yapay zeka hakkı doldu', + 'mailInbox.reason.aiDisabled': 'Yapay zekasız okundu, rezervasyon bulunamadı', + 'mailInbox.reason.aiUserCap': 'Yapay zekayla okunan e-postalar için günlük sınırına ulaştın', + 'mailInbox.reason.aiProviderError': 'Yapay zeka şu anda okuyamadı', + 'mailInbox.reason.noTripInRange': 'Bu tarihleri kapsayan bir seyahat yok', + 'mailInbox.reason.tripAmbiguous': 'Birden fazla seyahat kastedilmiş olabilir', + 'mailInbox.reason.tripWithoutDates': 'Seyahatin karşılaştırılacak tarihi yok', + 'mailInbox.reason.noEditPermission': 'Bu seyahate rezervasyon ekleme iznin yok', + 'mailInbox.reason.demoMode': 'Demo rezervasyon eklemez', + 'mailInbox.reason.outsideTripDates': 'Bir tarih seyahatin dışında kalıyor', + 'mailInbox.reason.sourceMismatch': 'E-postadaki bilgiler birbiriyle çelişiyor', + 'mailInbox.reason.implausible': 'Bir değer doğru olamaz', + 'mailInbox.reason.notInText': 'Okunan bir değer e-postada yok', + 'mailInbox.reason.imageOnly': 'Yalnızca görselden okundu, bu yüzden bakman gerekiyor', + 'mailInbox.reason.missingCriticalField': 'Tür, tarih veya güzergah eksik', + 'mailInbox.reason.missingConfirmation': 'Rezervasyon numarası bulunamadı', + 'mailInbox.reason.tooManyBookings': 'Tek e-postada kendiliğinden eklenenden fazla rezervasyon var', + 'mailInbox.reason.changeOrCancellation': 'Önceki bir rezervasyonun değişikliği veya iptali', + 'mailInbox.reason.cancelled': 'Rezervasyon iptal edildi', + 'mailInbox.reason.possibleDuplicate': 'Seyahatte bu rezervasyon zaten olabilir', + 'mailInbox.reason.changeAvailable': 'Seyahatte bu rezervasyon var, e-posta onu değiştiriyor', + 'mailInbox.reason.conflict': 'Seyahatte bu rezervasyon farklı bilgilerle var', + 'mailInbox.reason.askFirst': 'Her rezervasyonu onaylamayı istedin', + 'mailInbox.settings.title': 'Rezervasyon e-postaları', + 'mailInbox.settings.on': 'Açık', + 'mailInbox.settings.off': 'Kapalı', + 'mailInbox.settings.yourAddress': 'Adresin', + 'mailInbox.settings.copy': 'Adresi kopyala', + 'mailInbox.settings.copyError': 'Kopyalanamadı', + 'mailInbox.settings.renew': 'Yeni adres', + 'mailInbox.settings.renewConfirm': 'Tekrar dokun: eski adres çalışmayı bırakır', + 'mailInbox.settings.renewed': 'Yeni adresin hazır', + 'mailInbox.settings.saveError': 'Kaydedilemedi', + 'mailInbox.settings.addressHint': + 'Rezervasyon onaylarını bu adrese yönlendir, TREK onları doğru seyahate ekler. Bir parola gibi kendine sakla.', + 'mailInbox.settings.howTo': 'Otomatik yönlendir', + 'mailInbox.settings.howToGmail': + 'Gmail: Ayarlar, Yönlendirme ve POP/IMAP, Yönlendirme adresi ekle. Gmail bir kod gönderir, kod Bekleyen rezervasyonlar altında görünür. Ardından rezervasyon gönderenleri için TREK’e yönlendiren bir filtre ekle.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Ayarlar, E-posta, Kurallar, rezervasyon e-postalarını yönlendiren bir kural ekle. Bazı iş hesapları otomatik yönlendirmeyi engeller; orada elle yönlendir.', + 'mailInbox.settings.howToManual': 'Elle: herhangi bir rezervasyon e-postasını ekleriyle birlikte yönlendir.', + 'mailInbox.settings.enabled': 'Rezervasyon e-postalarını içe aktar', + 'mailInbox.settings.enabledHint': 'Kapalıyken adresine gelen e-postalar okunmaz.', + 'mailInbox.settings.ai': 'Yapay zekayla oku', + 'mailInbox.settings.aiHint': + 'Her rezervasyon e-postasını okur ve seyahatini bulur. E-posta ile seyahatlerinin tarihleri ve varış yerleri yapay zeka sağlayıcısına gider.', + 'mailInbox.settings.aiUnavailable': + 'Burada kullanılamıyor: yalnızca makine tarafından okunabilir rezervasyon verisi içeren e-postalar okunur.', + 'mailInbox.settings.autoAdd': 'Kendiliğinden ekle', + 'mailInbox.settings.autoAddHint': + 'TREK’in emin olduğu bir rezervasyon doğrudan seyahatine girer ve 7 gün içinde geri alabilirsin. Kapalıyken her biri onayını bekler.', + 'mailInbox.settings.upload': 'E-posta dosyası yükle', + 'mailInbox.settings.uploaded': 'E-posta alındı, okunuyor', + 'mailInbox.settings.uploadDuplicate': 'Bu e-posta zaten eklenmiş', + 'mailInbox.settings.uploadError': 'Dosya e-posta olarak okunamadı', + 'mailInbox.settings.privacy': + 'Yönlendirilen e-postalar en fazla 30 gün şifreli saklanır, sonra silinir. Lütfen kimlik veya sağlık belgelerini yönlendirme.', + 'mailInbox.trip.title': 'Ya da e-postayla yönlendir', + 'mailInbox.trip.hint': 'Bu seyahatin adresine gönderilen rezervasyon e-postası tahmin yürütülmeden buraya düşer.', + 'mailInbox.trip.create': 'Bu seyahat için adres oluştur', + 'mailInbox.fromMail': 'E-postadan', + 'mailInbox.fromMailHint': 'Yönlendirilen bir rezervasyon e-postasından eklendi', +}; +export default mailInbox; diff --git a/shared/src/i18n/tr/notif.ts b/shared/src/i18n/tr/notif.ts index cec74ae91..14c8ac44a 100644 --- a/shared/src/i18n/tr/notif.ts +++ b/shared/src/i18n/tr/notif.ts @@ -60,7 +60,8 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.shared': '{actor}, iletilen bir e-postadan {booking} rezervasyonunu {trip} seyahatine ekledi', 'notif.mail_booking_pending.title': 'İncelenecek iletilen rezervasyonlar', - 'notif.mail_booking_pending.text': 'Seni bekleyen iletilen rezervasyonlar: {count}', + 'notif.mail_booking_pending.text': '{count} iletilen rezervasyon seni bekliyor', + 'notif.mail_booking_pending.text.one': '{count} iletilen rezervasyon seni bekliyor', 'notif.mail_ingest_paused.title': 'E-posta içe aktarımı duraklatıldı', 'notif.mail_ingest_paused.text': 'İletilen rezervasyon e-postaları şimdilik kabul edilmiyor. Adreslerin kalıyor ve içe aktarım geri geldiğinde yeniden çalışacak.', diff --git a/shared/src/i18n/tr/reservations.ts b/shared/src/i18n/tr/reservations.ts index 06f83634d..308307365 100644 --- a/shared/src/i18n/tr/reservations.ts +++ b/shared/src/i18n/tr/reservations.ts @@ -36,7 +36,10 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Aktarma', 'reservations.layover.layover': 'Aktarma bekleme süresi', 'reservations.needsReview': 'Gözden geçirmek', - 'reservations.needsReviewHint': 'Havaalanı otomatik olarak eşleştirilemedi; lütfen konumu onaylayın.', + 'reservations.needsReviewHint': 'Bazı bilgiler otomatik dolduruldu ve kesin okunamadı. Lütfen kontrol et.', + 'reservations.reviewConfirm': 'Doğru görünüyor', + 'reservations.reviewConfirmHint': 'Kontrol edildi olarak işaretlemek için tıkla.', + 'reservations.reviewConfirmed': 'Kontrol edildi olarak işaretlendi', 'reservations.searchLocation': 'İstasyonu, limanı, adresi arayın…', 'reservations.meta.trainNumber': 'Tren No.', 'reservations.meta.platform': 'Platformu', diff --git a/shared/src/i18n/tr/settings.ts b/shared/src/i18n/tr/settings.ts index 3bb4f2da5..a33983109 100644 --- a/shared/src/i18n/tr/settings.ts +++ b/shared/src/i18n/tr/settings.ts @@ -117,6 +117,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Rötarlar', 'settings.notifyFlightLanded': 'İnişler', 'settings.notifyTripReminder': 'Seyahat hatırlatıcıları', + 'settings.notifyMailBookingAdded': 'E-postalardan eklenen rezervasyonlar', + 'settings.notifyMailBookingPending': 'İncelenecek iletilen rezervasyonlar', + 'settings.notifyMailIngestPaused': 'E-posta içe aktarma duraklatıldı', 'settings.notifyTodoDue': 'Yapılacak vadesi yakında', 'settings.notifyVacayInvite': 'Vacay birleştirme davetleri', 'settings.notifyVacayShare': 'Vacay takvim paylaşımları', diff --git a/shared/src/i18n/uk/index.ts b/shared/src/i18n/uk/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/uk/index.ts +++ b/shared/src/i18n/uk/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/uk/mailAdmin.ts b/shared/src/i18n/uk/mailAdmin.ts new file mode 100644 index 000000000..0470a5f71 --- /dev/null +++ b/shared/src/i18n/uk/mailAdmin.ts @@ -0,0 +1,47 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Імпорт листів із бронюваннями', + 'mailAdmin.hint': 'Переслані підтвердження бронювань стають бронюваннями в потрібній подорожі.', + 'mailAdmin.enabled': 'Імпортувати листи з бронюваннями', + 'mailAdmin.enabledHint': + 'Дає кожному користувачу цієї інсталяції адресу для пересилання. Якщо вимкнено, жодна адреса не приймає пошту.', + 'mailAdmin.transport.label': 'Як надходять листи', + 'mailAdmin.transport.off': 'Лише завантаження', + 'mailAdmin.transport.imap': 'Поштова скринька IMAP', + 'mailAdmin.transport.webhook': 'Webhook', + 'mailAdmin.aiDaily': 'Листів із ШІ на особу за день', + 'mailAdmin.aiDailyHint': 'Понад цей ліміт читаються лише машинозчитувані дані про бронювання.', + 'mailAdmin.template': 'Шаблон адреси', + 'mailAdmin.templateHint': 'Адреса для пересилання, де {token} замінюється кодом кожного користувача.', + 'mailAdmin.imap.host': 'Сервер', + 'mailAdmin.imap.port': 'Порт', + 'mailAdmin.imap.tls': 'Шифрування', + 'mailAdmin.imap.user': 'Користувач', + 'mailAdmin.imap.pass': 'Пароль', + 'mailAdmin.imap.mailbox': 'Тека', + 'mailAdmin.imap.mailboxHint': 'Прочитані листи переміщуються до TREK/processed.', + 'mailAdmin.webhook.secret': 'Ключ підпису', + 'mailAdmin.webhook.secretHint': + 'Щонайменше 32 символи; відправник підписує ним кожен лист. Порожній ключ вимикає webhook.', + 'mailAdmin.inboundUrl': 'Адреса webhook', + 'mailAdmin.inboundUrlHint': 'Ваша поштова служба надсилає сюди кожен лист у початковому вигляді й з підписом.', + 'mailAdmin.copyUrl': 'Копіювати адресу', + 'mailAdmin.envHint': 'Змінні середовища (MAIL_INGEST_*) мають пріоритет над цими полями.', + 'mailAdmin.loadError': 'Не вдалося завантажити налаштування імпорту листів.', + 'mailAdmin.problem.secretShort': 'Ключ має містити щонайменше 32 символи.', + 'mailAdmin.problem.portInvalid': 'Введіть порт від 1 до 65535.', + 'mailAdmin.problem.templateToken': 'Адреса має містити {token} для коду кожного користувача.', + 'mailAdmin.problem.dailyInvalid': 'Введіть ціле число від 0 до 1000.', + 'mailAdmin.status.title': 'Статус', + 'mailAdmin.status.lastRead': 'Останнє читання {time}', + 'mailAdmin.status.never': 'ніколи', + 'mailAdmin.status.mails': 'Листи, 24 год', + 'mailAdmin.status.added': 'Додано самостійно', + 'mailAdmin.status.waiting': 'Очікують', + 'mailAdmin.status.failed': 'Помилки', + 'mailAdmin.test.run': 'Перевірити з’єднання', + 'mailAdmin.test.ok': 'З’єднано', + 'mailAdmin.test.failed': 'Немає з’єднання', +}; +export default mailAdmin; diff --git a/shared/src/i18n/uk/mailInbox.ts b/shared/src/i18n/uk/mailInbox.ts new file mode 100644 index 000000000..33f908a92 --- /dev/null +++ b/shared/src/i18n/uk/mailInbox.ts @@ -0,0 +1,100 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Бронювання на перевірці', + 'mailInbox.pendingCount': '{count} бронювання чекає на вас', + 'mailInbox.pendingCount.one': '{count} бронювання чекає на вас', + 'mailInbox.pendingCount.few': '{count} бронювання чекають на вас', + 'mailInbox.pendingCount.many': '{count} бронювань чекає на вас', + 'mailInbox.empty': 'Нічого не чекає', + 'mailInbox.emptyHint': + 'Бронювання, які TREK не зміг додати сам, з’являться тут разом із варіантами, що з ними можна зробити.', + 'mailInbox.untitled': 'Лист із бронюванням', + 'mailInbox.showMail': 'Показати лист', + 'mailInbox.backToList': 'Назад до списку', + 'mailInbox.loadMore': 'Показати ще', + 'mailInbox.pickTrip': 'Яка подорож?', + 'mailInbox.added': 'Бронювання додано', + 'mailInbox.filed': 'Збережено у файлах подорожі', + 'mailInbox.discarded': 'Відхилено', + 'mailInbox.actionError': 'Не вийшло. Спробуйте ще раз трохи згодом.', + 'mailInbox.previewError': 'Не вдалося показати лист.', + 'mailInbox.action.add': 'Додати', + 'mailInbox.action.choose': 'Додати до подорожі', + 'mailInbox.action.file': 'Зберегти як документ', + 'mailInbox.action.manual': 'Заповнити самостійно', + 'mailInbox.action.discard': 'Відхилити', + 'mailInbox.reason.forwardingConfirmation': + 'Ваш поштовий сервіс просить підтвердити пересилання. Введіть там цей код.', + 'mailInbox.reason.newsletter': 'Розсилка, а не бронювання', + 'mailInbox.reason.autoReply': 'Автоматична відповідь', + 'mailInbox.reason.emptyMail': 'У листі немає тексту чи вкладення для читання', + 'mailInbox.reason.mailUnreadable': 'Не вдалося прочитати лист', + 'mailInbox.reason.notABooking': 'У цьому листі бронювання не знайдено', + 'mailInbox.reason.tooLarge': 'Лист завеликий для читання', + 'mailInbox.reason.addressInvalid': 'Він надійшов на адресу, яка більше не працює', + 'mailInbox.reason.addressDailyLimit': 'Ваша адреса сьогодні прийняла максимум листів', + 'mailInbox.reason.processingFailed': 'Прочитати лист не вдалося кілька разів', + 'mailInbox.reason.aiQuotaExhausted': 'Ліміт ШІ на цей період вичерпано', + 'mailInbox.reason.aiDisabled': 'Прочитано без ШІ, бронювання не знайдено', + 'mailInbox.reason.aiUserCap': 'Досягнуто денного ліміту листів, які читає ШІ', + 'mailInbox.reason.aiProviderError': 'ШІ зараз не зміг це прочитати', + 'mailInbox.reason.noTripInRange': 'Жодна подорож не охоплює ці дати', + 'mailInbox.reason.tripAmbiguous': 'Може йтися про більш ніж одну подорож', + 'mailInbox.reason.tripWithoutDates': 'У подорожі немає дат для звірки', + 'mailInbox.reason.noEditPermission': 'Вам не можна додавати бронювання до цієї подорожі', + 'mailInbox.reason.demoMode': 'Демо не додає бронювань', + 'mailInbox.reason.outsideTripDates': 'Одна з дат виходить за межі подорожі', + 'mailInbox.reason.sourceMismatch': 'Дані листа суперечать одне одному', + 'mailInbox.reason.implausible': 'Одне зі значень не може бути правильним', + 'mailInbox.reason.notInText': 'Зчитаного значення немає в листі', + 'mailInbox.reason.imageOnly': 'Зчитано лише із зображення, тому потрібна ваша перевірка', + 'mailInbox.reason.missingCriticalField': 'Бракує типу, дати або маршруту', + 'mailInbox.reason.missingConfirmation': 'Номер бронювання не знайдено', + 'mailInbox.reason.tooManyBookings': 'В одному листі більше бронювань, ніж додається автоматично', + 'mailInbox.reason.changeOrCancellation': 'Зміна або скасування попереднього бронювання', + 'mailInbox.reason.cancelled': 'Бронювання скасовано', + 'mailInbox.reason.possibleDuplicate': 'Можливо, подорож уже містить це бронювання', + 'mailInbox.reason.changeAvailable': 'Подорож містить це бронювання, а лист його змінює', + 'mailInbox.reason.conflict': 'Подорож містить це бронювання з іншими даними', + 'mailInbox.reason.askFirst': 'Ви просили підтверджувати кожне бронювання', + 'mailInbox.settings.title': 'Листи з бронюваннями', + 'mailInbox.settings.on': 'Увімк.', + 'mailInbox.settings.off': 'Вимк.', + 'mailInbox.settings.yourAddress': 'Ваша адреса', + 'mailInbox.settings.copy': 'Копіювати адресу', + 'mailInbox.settings.copyError': 'Не вдалося скопіювати', + 'mailInbox.settings.renew': 'Нова адреса', + 'mailInbox.settings.renewConfirm': 'Натисніть ще раз: стара адреса перестане працювати', + 'mailInbox.settings.renewed': 'Ваша нова адреса готова', + 'mailInbox.settings.saveError': 'Не вдалося зберегти', + 'mailInbox.settings.addressHint': + 'Пересилайте підтвердження бронювань на цю адресу, і TREK додасть їх до потрібної подорожі. Зберігайте її в таємниці, як пароль.', + 'mailInbox.settings.howTo': 'Пересилати автоматично', + 'mailInbox.settings.howToGmail': + 'Gmail: Налаштування, Пересилання та POP/IMAP, Додати адресу пересилання. Gmail надішле код, який з’явиться в розділі Бронювання на перевірці. Потім додайте фільтр для відправників бронювань із пересиланням на TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Параметри, Пошта, Правила, додайте правило, яке пересилає листи з бронюваннями. Деякі робочі облікові записи блокують автоматичне пересилання, там пересилайте вручну.', + 'mailInbox.settings.howToManual': 'Вручну: перешліть будь-який лист із бронюванням разом із вкладеннями.', + 'mailInbox.settings.enabled': 'Імпортувати листи з бронюваннями', + 'mailInbox.settings.enabledHint': 'Якщо вимкнено, листи на вашу адресу не читаються.', + 'mailInbox.settings.ai': 'Читати за допомогою ШІ', + 'mailInbox.settings.aiHint': + 'Читає будь-який лист із бронюванням і знаходить для нього подорож. Лист, а також дати й напрямки ваших подорожей передаються провайдеру ШІ.', + 'mailInbox.settings.aiUnavailable': 'Тут недоступно: читаються лише листи з машинозчитуваними даними про бронювання.', + 'mailInbox.settings.autoAdd': 'Додавати самостійно', + 'mailInbox.settings.autoAddHint': + 'Бронювання, у якому TREK певен, одразу потрапляє до подорожі, і 7 днів його можна скасувати. Якщо вимкнено, кожне чекає на ваше підтвердження.', + 'mailInbox.settings.upload': 'Завантажити файл листа', + 'mailInbox.settings.uploaded': 'Лист отримано, він обробляється', + 'mailInbox.settings.uploadDuplicate': 'Цей лист уже додано', + 'mailInbox.settings.uploadError': 'Не вдалося прочитати файл як лист', + 'mailInbox.settings.privacy': + 'Переслані листи зберігаються в зашифрованому вигляді не довше 30 днів, потім видаляються. Будь ласка, не пересилайте документи, що посвідчують особу, та медичні документи.', + 'mailInbox.trip.title': 'Або перешліть поштою', + 'mailInbox.trip.hint': 'Лист із бронюванням, надісланий на адресу цієї подорожі, потрапляє сюди без здогадок.', + 'mailInbox.trip.create': 'Створити адресу для цієї подорожі', + 'mailInbox.fromMail': 'З листа', + 'mailInbox.fromMailHint': 'Додано з пересланого листа з бронюванням', +}; +export default mailInbox; diff --git a/shared/src/i18n/uk/notif.ts b/shared/src/i18n/uk/notif.ts index daf0330f0..945f85d64 100644 --- a/shared/src/i18n/uk/notif.ts +++ b/shared/src/i18n/uk/notif.ts @@ -61,7 +61,10 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} додано до {trip}', 'notif.mail_booking_added.shared': '{actor} додав(ла) {booking} до {trip} з пересланого листа', 'notif.mail_booking_pending.title': 'Переслані бронювання для перевірки', - 'notif.mail_booking_pending.text': 'Пересланих бронювань чекає на вас: {count}', + 'notif.mail_booking_pending.text': '{count} пересланого бронювання чекає на вас', + 'notif.mail_booking_pending.text.one': '{count} переслане бронювання чекає на вас', + 'notif.mail_booking_pending.text.few': '{count} переслані бронювання чекають на вас', + 'notif.mail_booking_pending.text.many': '{count} пересланих бронювань чекає на вас', 'notif.mail_ingest_paused.title': 'Імпорт листів призупинено', 'notif.mail_ingest_paused.text': 'Переслані листи з бронюваннями поки що не приймаються. Ваші адреси зберігаються й знову працюватимуть, коли імпорт повернеться.', diff --git a/shared/src/i18n/uk/reservations.ts b/shared/src/i18n/uk/reservations.ts index 67e938192..8903b3271 100644 --- a/shared/src/i18n/uk/reservations.ts +++ b/shared/src/i18n/uk/reservations.ts @@ -35,7 +35,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Пересадка', 'reservations.layover.layover': 'Очікування', 'reservations.needsReview': 'Перевірити', - 'reservations.needsReviewHint': 'Аеропорт не вдалося визначити автоматично — підтвердіть місцезнаходження.', + 'reservations.needsReviewHint': + 'Частину даних заповнено автоматично, і зчитано їх не зі стовідсотковою певністю. Будь ласка, перевірте їх.', + 'reservations.reviewConfirm': 'Усе вірно', + 'reservations.reviewConfirmHint': 'Натисніть, щоб позначити як перевірене.', + 'reservations.reviewConfirmed': 'Позначено як перевірене', 'reservations.searchLocation': 'Шукати станцію, порт, адресу...', 'reservations.meta.trainNumber': 'Номер поїзда', 'reservations.meta.platform': 'Платформа', diff --git a/shared/src/i18n/uk/settings.ts b/shared/src/i18n/uk/settings.ts index 8f16d78ef..63604cb5a 100644 --- a/shared/src/i18n/uk/settings.ts +++ b/shared/src/i18n/uk/settings.ts @@ -116,6 +116,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Затримки рейсів', 'settings.notifyFlightLanded': 'Посадки', 'settings.notifyTripReminder': 'Нагадування про поїздку', + 'settings.notifyMailBookingAdded': 'Бронювання, додані з листів', + 'settings.notifyMailBookingPending': 'Переслані бронювання для перевірки', + 'settings.notifyMailIngestPaused': 'Імпорт листів призупинено', 'settings.notifyTodoDue': 'Завдання до терміну', 'settings.notifyVacayInvite': "Запрошення об'єднання Vacay", 'settings.notifyVacayShare': 'Спільний доступ до календарів Vacay', diff --git a/shared/src/i18n/vi/index.ts b/shared/src/i18n/vi/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/vi/index.ts +++ b/shared/src/i18n/vi/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/vi/mailAdmin.ts b/shared/src/i18n/vi/mailAdmin.ts new file mode 100644 index 000000000..789fb9d48 --- /dev/null +++ b/shared/src/i18n/vi/mailAdmin.ts @@ -0,0 +1,46 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': 'Nhập email đặt chỗ', + 'mailAdmin.hint': 'Email xác nhận đặt chỗ được chuyển tiếp sẽ trở thành đặt chỗ trong đúng chuyến đi.', + 'mailAdmin.enabled': 'Nhập email đặt chỗ', + 'mailAdmin.enabledHint': + 'Cấp cho mọi người trên bản cài đặt này một địa chỉ chuyển tiếp. Khi tắt, không địa chỉ nào nhận email.', + 'mailAdmin.transport.label': 'Cách email đến', + 'mailAdmin.transport.off': 'Chỉ tải lên', + 'mailAdmin.transport.imap': 'Hộp thư IMAP', + 'mailAdmin.transport.webhook': 'Webhook', // same-as-en + 'mailAdmin.aiDaily': 'Số email AI mỗi người mỗi ngày', + 'mailAdmin.aiDailyHint': 'Vượt mức này, chỉ đọc dữ liệu đặt chỗ máy đọc được.', + 'mailAdmin.template': 'Mẫu địa chỉ', + 'mailAdmin.templateHint': 'Địa chỉ chuyển tiếp, với {token} ở vị trí mã của từng người.', + 'mailAdmin.imap.host': 'Máy chủ', + 'mailAdmin.imap.port': 'Cổng', + 'mailAdmin.imap.tls': 'Mã hóa', + 'mailAdmin.imap.user': 'Người dùng', + 'mailAdmin.imap.pass': 'Mật khẩu', + 'mailAdmin.imap.mailbox': 'Thư mục', + 'mailAdmin.imap.mailboxHint': 'Email đã đọc sẽ được chuyển sang TREK/processed.', + 'mailAdmin.webhook.secret': 'Khóa bí mật để ký', + 'mailAdmin.webhook.secretHint': 'Ít nhất 32 ký tự; bên gửi dùng khóa này để ký mọi email. Để trống sẽ tắt webhook.', + 'mailAdmin.inboundUrl': 'Địa chỉ webhook', + 'mailAdmin.inboundUrlHint': 'Dịch vụ email của bạn gửi từng email đến đây, ở dạng thô và đã ký.', + 'mailAdmin.copyUrl': 'Sao chép địa chỉ', + 'mailAdmin.envHint': 'Biến môi trường (MAIL_INGEST_*) được ưu tiên hơn các trường này.', + 'mailAdmin.loadError': 'Không tải được cài đặt nhập email.', + 'mailAdmin.problem.secretShort': 'Khóa bí mật cần ít nhất 32 ký tự.', + 'mailAdmin.problem.portInvalid': 'Nhập một cổng từ 1 đến 65535.', + 'mailAdmin.problem.templateToken': 'Địa chỉ cần có {token} ở vị trí mã của từng người.', + 'mailAdmin.problem.dailyInvalid': 'Nhập một số nguyên từ 0 đến 1000.', + 'mailAdmin.status.title': 'Trạng thái', + 'mailAdmin.status.lastRead': 'Đọc lần cuối {time}', + 'mailAdmin.status.never': 'chưa bao giờ', + 'mailAdmin.status.mails': 'Email, 24 giờ', + 'mailAdmin.status.added': 'Tự động thêm', + 'mailAdmin.status.waiting': 'Đang chờ', + 'mailAdmin.status.failed': 'Thất bại', + 'mailAdmin.test.run': 'Kiểm tra kết nối', + 'mailAdmin.test.ok': 'Đã kết nối', + 'mailAdmin.test.failed': 'Không kết nối được', +}; +export default mailAdmin; diff --git a/shared/src/i18n/vi/mailInbox.ts b/shared/src/i18n/vi/mailInbox.ts new file mode 100644 index 000000000..5a2bd8183 --- /dev/null +++ b/shared/src/i18n/vi/mailInbox.ts @@ -0,0 +1,97 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': 'Đặt chỗ đang chờ', + 'mailInbox.pendingCount': '{count} đặt chỗ đang chờ bạn xử lý', + 'mailInbox.empty': 'Không có gì đang chờ', + 'mailInbox.emptyHint': + 'Những đặt chỗ mà TREK không thể tự thêm sẽ hiện ở đây, kèm các cách bạn có thể xử lý từng mục.', + 'mailInbox.untitled': 'Email đặt chỗ', + 'mailInbox.showMail': 'Xem email', + 'mailInbox.backToList': 'Quay lại danh sách', + 'mailInbox.loadMore': 'Xem thêm', + 'mailInbox.pickTrip': 'Chuyến đi nào?', + 'mailInbox.added': 'Đã thêm đặt chỗ', + 'mailInbox.filed': 'Đã lưu vào tệp của chuyến đi', + 'mailInbox.discarded': 'Đã bỏ', + 'mailInbox.actionError': 'Không thực hiện được. Hãy thử lại sau giây lát.', + 'mailInbox.previewError': 'Không thể hiển thị email.', + 'mailInbox.action.add': 'Thêm', + 'mailInbox.action.choose': 'Thêm vào chuyến đi', + 'mailInbox.action.file': 'Lưu làm tài liệu', + 'mailInbox.action.manual': 'Tự điền', + 'mailInbox.action.discard': 'Bỏ', + 'mailInbox.reason.forwardingConfirmation': + 'Nhà cung cấp email của bạn yêu cầu xác nhận việc chuyển tiếp. Hãy nhập mã này ở đó.', + 'mailInbox.reason.newsletter': 'Đây là bản tin, không phải đặt chỗ', + 'mailInbox.reason.autoReply': 'Đây là thư trả lời tự động', + 'mailInbox.reason.emptyMail': 'Email không có nội dung hay tệp đính kèm để đọc', + 'mailInbox.reason.mailUnreadable': 'Không đọc được email', + 'mailInbox.reason.notABooking': 'Không tìm thấy đặt chỗ nào trong email này', + 'mailInbox.reason.tooLarge': 'Email quá lớn để đọc', + 'mailInbox.reason.addressInvalid': 'Email gửi đến một địa chỉ không còn dùng được', + 'mailInbox.reason.addressDailyLimit': 'Địa chỉ của bạn đã nhận đủ số email cho hôm nay', + 'mailInbox.reason.processingFailed': 'Đọc thất bại nhiều lần', + 'mailInbox.reason.aiQuotaExhausted': 'Hạn mức AI của kỳ này đã dùng hết', + 'mailInbox.reason.aiDisabled': 'Đã đọc không dùng AI và không tìm thấy đặt chỗ', + 'mailInbox.reason.aiUserCap': 'Đã đạt giới hạn email đọc bằng AI trong ngày của bạn', + 'mailInbox.reason.aiProviderError': 'AI không đọc được vào lúc này', + 'mailInbox.reason.noTripInRange': 'Không có chuyến đi nào bao gồm những ngày này', + 'mailInbox.reason.tripAmbiguous': 'Có thể là nhiều hơn một chuyến đi', + 'mailInbox.reason.tripWithoutDates': 'Chuyến đi không có ngày để đối chiếu', + 'mailInbox.reason.noEditPermission': 'Bạn không có quyền thêm đặt chỗ vào chuyến đi đó', + 'mailInbox.reason.demoMode': 'Bản demo không thêm đặt chỗ', + 'mailInbox.reason.outsideTripDates': 'Có một ngày nằm ngoài chuyến đi', + 'mailInbox.reason.sourceMismatch': 'Các chi tiết trong email mâu thuẫn với nhau', + 'mailInbox.reason.implausible': 'Có một giá trị không thể đúng', + 'mailInbox.reason.notInText': 'Một giá trị đọc được không có trong email', + 'mailInbox.reason.imageOnly': 'Chỉ đọc được từ hình ảnh nên cần bạn xem lại', + 'mailInbox.reason.missingCriticalField': 'Thiếu loại, ngày hoặc lộ trình', + 'mailInbox.reason.missingConfirmation': 'Không tìm thấy mã đặt chỗ', + 'mailInbox.reason.tooManyBookings': 'Một email chứa nhiều đặt chỗ hơn mức được tự động thêm', + 'mailInbox.reason.changeOrCancellation': 'Thay đổi hoặc hủy một đặt chỗ trước đó', + 'mailInbox.reason.cancelled': 'Đặt chỗ đã bị hủy', + 'mailInbox.reason.possibleDuplicate': 'Chuyến đi có thể đã có đặt chỗ này', + 'mailInbox.reason.changeAvailable': 'Chuyến đi đã có đặt chỗ này và email này thay đổi nó', + 'mailInbox.reason.conflict': 'Chuyến đi đã có đặt chỗ này nhưng với chi tiết khác', + 'mailInbox.reason.askFirst': 'Bạn đã chọn xác nhận từng đặt chỗ', + 'mailInbox.settings.title': 'Email đặt chỗ', + 'mailInbox.settings.on': 'Bật', + 'mailInbox.settings.off': 'Tắt', + 'mailInbox.settings.yourAddress': 'Địa chỉ của bạn', + 'mailInbox.settings.copy': 'Sao chép địa chỉ', + 'mailInbox.settings.copyError': 'Không sao chép được', + 'mailInbox.settings.renew': 'Địa chỉ mới', + 'mailInbox.settings.renewConfirm': 'Chạm lần nữa: địa chỉ cũ sẽ ngừng hoạt động', + 'mailInbox.settings.renewed': 'Địa chỉ mới của bạn đã sẵn sàng', + 'mailInbox.settings.saveError': 'Không lưu được', + 'mailInbox.settings.addressHint': + 'Chuyển tiếp email xác nhận đặt chỗ đến địa chỉ này và TREK sẽ thêm chúng vào đúng chuyến đi. Hãy giữ kín như mật khẩu.', + 'mailInbox.settings.howTo': 'Chuyển tiếp tự động', + 'mailInbox.settings.howToGmail': + 'Gmail: Cài đặt, Chuyển tiếp và POP/IMAP, Thêm địa chỉ chuyển tiếp. Gmail sẽ gửi một mã, mã này hiện trong mục Đặt chỗ đang chờ. Sau đó hãy thêm bộ lọc cho những người gửi email đặt chỗ để chuyển tiếp đến TREK.', + 'mailInbox.settings.howToOutlook': + 'Outlook: Cài đặt, Thư, Quy tắc, thêm một quy tắc chuyển tiếp email đặt chỗ. Một số tài khoản cơ quan chặn chuyển tiếp tự động; khi đó hãy chuyển tiếp thủ công.', + 'mailInbox.settings.howToManual': 'Thủ công: chuyển tiếp bất kỳ email đặt chỗ nào, kèm cả tệp đính kèm.', + 'mailInbox.settings.enabled': 'Nhập email đặt chỗ', + 'mailInbox.settings.enabledHint': 'Khi tắt, email gửi đến địa chỉ của bạn sẽ không được đọc.', + 'mailInbox.settings.ai': 'Đọc bằng AI', + 'mailInbox.settings.aiHint': + 'Đọc mọi email đặt chỗ và tìm chuyến đi tương ứng. Email cùng ngày và điểm đến của các chuyến đi của bạn sẽ được gửi đến nhà cung cấp AI.', + 'mailInbox.settings.aiUnavailable': 'Không khả dụng ở đây: chỉ đọc những email có dữ liệu đặt chỗ máy đọc được.', + 'mailInbox.settings.autoAdd': 'Tự động thêm', + 'mailInbox.settings.autoAddHint': + 'Đặt chỗ mà TREK chắc chắn sẽ vào thẳng chuyến đi, và bạn có thể hoàn tác trong 7 ngày. Khi tắt, mỗi đặt chỗ sẽ chờ bạn đồng ý.', + 'mailInbox.settings.upload': 'Tải lên tệp email', + 'mailInbox.settings.uploaded': 'Đã nhận email, đang đọc', + 'mailInbox.settings.uploadDuplicate': 'Email này đã có rồi', + 'mailInbox.settings.uploadError': 'Không thể đọc tệp này như một email', + 'mailInbox.settings.privacy': + 'Email được chuyển tiếp sẽ được lưu mã hóa tối đa 30 ngày rồi bị xóa. Vui lòng không chuyển tiếp giấy tờ tùy thân hoặc hồ sơ sức khỏe.', + 'mailInbox.trip.title': 'Hoặc chuyển tiếp qua email', + 'mailInbox.trip.hint': 'Email đặt chỗ gửi đến địa chỉ của chuyến đi này sẽ vào thẳng đây, không cần đoán.', + 'mailInbox.trip.create': 'Tạo địa chỉ cho chuyến đi này', + 'mailInbox.fromMail': 'Từ email', + 'mailInbox.fromMailHint': 'Được thêm từ một email đặt chỗ đã chuyển tiếp', +}; +export default mailInbox; diff --git a/shared/src/i18n/vi/notif.ts b/shared/src/i18n/vi/notif.ts index ef8190de9..9808bc962 100644 --- a/shared/src/i18n/vi/notif.ts +++ b/shared/src/i18n/vi/notif.ts @@ -58,7 +58,7 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': 'Đã thêm {booking} vào {trip}', 'notif.mail_booking_added.shared': '{actor} đã thêm {booking} vào {trip} từ một email được chuyển tiếp', 'notif.mail_booking_pending.title': 'Đặt chỗ chuyển tiếp cần xem lại', - 'notif.mail_booking_pending.text': 'Đặt chỗ chuyển tiếp đang chờ bạn: {count}', + 'notif.mail_booking_pending.text': '{count} đặt chỗ được chuyển tiếp đang chờ bạn', 'notif.mail_ingest_paused.title': 'Đã tạm dừng nhập email', 'notif.mail_ingest_paused.text': 'Hiện tại email đặt chỗ được chuyển tiếp chưa được nhận. Địa chỉ của bạn vẫn được giữ và sẽ hoạt động lại khi tính năng nhập quay lại.', diff --git a/shared/src/i18n/vi/reservations.ts b/shared/src/i18n/vi/reservations.ts index 16397d8b7..e8bec4e88 100644 --- a/shared/src/i18n/vi/reservations.ts +++ b/shared/src/i18n/vi/reservations.ts @@ -36,7 +36,11 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': 'Sự liên quan', 'reservations.layover.layover': 'quá cảnh', 'reservations.needsReview': 'Ôn tập', - 'reservations.needsReviewHint': 'Sân bay không thể được khớp tự động - vui lòng xác nhận vị trí.', + 'reservations.needsReviewHint': + 'Một số chi tiết được điền tự động và không thể đọc chắc chắn. Vui lòng kiểm tra lại.', + 'reservations.reviewConfirm': 'Đúng rồi', + 'reservations.reviewConfirmHint': 'Nhấp để đánh dấu là đã kiểm tra.', + 'reservations.reviewConfirmed': 'Đã đánh dấu là đã kiểm tra', 'reservations.searchLocation': 'Trạm tìm kiếm, cổng, địa chỉ…', 'reservations.meta.trainNumber': 'Số tàu', 'reservations.meta.platform': 'Sân ga', diff --git a/shared/src/i18n/vi/settings.ts b/shared/src/i18n/vi/settings.ts index b0bdde1da..b2ddf72cc 100644 --- a/shared/src/i18n/vi/settings.ts +++ b/shared/src/i18n/vi/settings.ts @@ -121,6 +121,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': 'Chuyến bay bị trễ', 'settings.notifyFlightLanded': 'Chuyến bay hạ cánh', 'settings.notifyTripReminder': 'Lời nhắc chuyến đi', + 'settings.notifyMailBookingAdded': 'Đặt chỗ được thêm từ email', + 'settings.notifyMailBookingPending': 'Đặt chỗ chuyển tiếp cần xem xét', + 'settings.notifyMailIngestPaused': 'Nhập email bị tạm dừng', 'settings.notifyTodoDue': 'Việc cần làm sắp đến hạn', 'settings.notifyVacayInvite': 'Lời mời kết hợp Vacay', 'settings.notifyVacayShare': 'Chia sẻ lịch Vacay', diff --git a/shared/src/i18n/zh-TW/index.ts b/shared/src/i18n/zh-TW/index.ts index 4ca140941..3230e9ff4 100644 --- a/shared/src/i18n/zh-TW/index.ts +++ b/shared/src/i18n/zh-TW/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/zh-TW/mailAdmin.ts b/shared/src/i18n/zh-TW/mailAdmin.ts new file mode 100644 index 000000000..a2e6af21e --- /dev/null +++ b/shared/src/i18n/zh-TW/mailAdmin.ts @@ -0,0 +1,45 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': '預訂郵件匯入', + 'mailAdmin.hint': '轉寄的預訂確認郵件會變成正確旅行中的預訂。', + 'mailAdmin.enabled': '匯入預訂郵件', + 'mailAdmin.enabledHint': '為此執行個體上的每個人提供一個轉寄地址。關閉後,任何地址都不接收郵件。', + 'mailAdmin.transport.label': '郵件送達方式', + 'mailAdmin.transport.off': '僅上傳', + 'mailAdmin.transport.imap': 'IMAP 信箱', + 'mailAdmin.transport.webhook': 'Webhook', + 'mailAdmin.aiDaily': '每人每天的 AI 郵件數', + 'mailAdmin.aiDailyHint': '超過後,只讀取機器可讀的預訂資料。', + 'mailAdmin.template': '地址範本', + 'mailAdmin.templateHint': '轉寄地址,在每個人的驗證碼所在位置使用 {token}。', + 'mailAdmin.imap.host': '伺服器', + 'mailAdmin.imap.port': '連接埠', + 'mailAdmin.imap.tls': '加密', + 'mailAdmin.imap.user': '使用者', + 'mailAdmin.imap.pass': '密碼', + 'mailAdmin.imap.mailbox': '資料夾', + 'mailAdmin.imap.mailboxHint': '已讀取的郵件會移到 TREK/processed。', + 'mailAdmin.webhook.secret': '簽章密鑰', + 'mailAdmin.webhook.secretHint': '至少 32 個字元;寄件端用它為每封郵件簽章。留空會關閉 Webhook。', + 'mailAdmin.inboundUrl': 'Webhook 地址', + 'mailAdmin.inboundUrlHint': '你的郵件服務會把每封郵件以原始且已簽章的形式傳送到這裡。', + 'mailAdmin.copyUrl': '複製地址', + 'mailAdmin.envHint': '環境變數(MAIL_INGEST_*)的優先順序高於這些欄位。', + 'mailAdmin.loadError': '無法載入郵件匯入設定。', + 'mailAdmin.problem.secretShort': '密鑰至少需要 32 個字元。', + 'mailAdmin.problem.portInvalid': '請輸入 1 到 65535 之間的連接埠。', + 'mailAdmin.problem.templateToken': '地址中需要在每個人的驗證碼所在位置使用 {token}。', + 'mailAdmin.problem.dailyInvalid': '請輸入 0 到 1000 之間的整數。', + 'mailAdmin.status.title': '狀態', + 'mailAdmin.status.lastRead': '上次讀取 {time}', + 'mailAdmin.status.never': '從未', + 'mailAdmin.status.mails': '郵件,24 小時', + 'mailAdmin.status.added': '自動加入', + 'mailAdmin.status.waiting': '等待中', + 'mailAdmin.status.failed': '失敗', + 'mailAdmin.test.run': '測試連線', + 'mailAdmin.test.ok': '已連線', + 'mailAdmin.test.failed': '無法連線', +}; +export default mailAdmin; diff --git a/shared/src/i18n/zh-TW/mailInbox.ts b/shared/src/i18n/zh-TW/mailInbox.ts new file mode 100644 index 000000000..6ea84626f --- /dev/null +++ b/shared/src/i18n/zh-TW/mailInbox.ts @@ -0,0 +1,91 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': '待處理的預訂', + 'mailInbox.pendingCount': '有 {count} 筆預訂等你處理', + 'mailInbox.empty': '沒有待處理的內容', + 'mailInbox.emptyHint': 'TREK 無法自動加入的預訂會顯示在這裡,並附上每一筆可以怎麼處理。', + 'mailInbox.untitled': '預訂郵件', + 'mailInbox.showMail': '查看郵件', + 'mailInbox.backToList': '返回清單', + 'mailInbox.loadMore': '顯示更多', + 'mailInbox.pickTrip': '哪一趟旅行?', + 'mailInbox.added': '已加入預訂', + 'mailInbox.filed': '已儲存到旅行的檔案中', + 'mailInbox.discarded': '已捨棄', + 'mailInbox.actionError': '操作沒有成功,請稍後再試。', + 'mailInbox.previewError': '無法顯示這封郵件。', + 'mailInbox.action.add': '加入', + 'mailInbox.action.choose': '加入旅行', + 'mailInbox.action.file': '儲存為文件', + 'mailInbox.action.manual': '自己填寫', + 'mailInbox.action.discard': '捨棄', + 'mailInbox.reason.forwardingConfirmation': '你的郵件服務商要求確認轉寄。請在那裡輸入此驗證碼。', + 'mailInbox.reason.newsletter': '這是電子報,不是預訂', + 'mailInbox.reason.autoReply': '這是自動回覆', + 'mailInbox.reason.emptyMail': '郵件中沒有可讀取的內文或附件', + 'mailInbox.reason.mailUnreadable': '無法讀取這封郵件', + 'mailInbox.reason.notABooking': '這封郵件中找不到預訂', + 'mailInbox.reason.tooLarge': '郵件太大,無法讀取', + 'mailInbox.reason.addressInvalid': '郵件寄到了已失效的地址', + 'mailInbox.reason.addressDailyLimit': '你的地址今天接收的郵件已達上限', + 'mailInbox.reason.processingFailed': '多次讀取失敗', + 'mailInbox.reason.aiQuotaExhausted': '本週期的 AI 額度已用完', + 'mailInbox.reason.aiDisabled': '未使用 AI 讀取,沒有找到預訂', + 'mailInbox.reason.aiUserCap': '已達到你每天用 AI 讀取郵件的上限', + 'mailInbox.reason.aiProviderError': 'AI 暫時無法讀取', + 'mailInbox.reason.noTripInRange': '沒有涵蓋這些日期的旅行', + 'mailInbox.reason.tripAmbiguous': '可能對應不只一趟旅行', + 'mailInbox.reason.tripWithoutDates': '旅行沒有可供核對的日期', + 'mailInbox.reason.noEditPermission': '你無權在該旅行中加入預訂', + 'mailInbox.reason.demoMode': '示範模式不會加入預訂', + 'mailInbox.reason.outsideTripDates': '有日期不在旅行範圍內', + 'mailInbox.reason.sourceMismatch': '郵件中的資訊互相矛盾', + 'mailInbox.reason.implausible': '有一個值不可能是正確的', + 'mailInbox.reason.notInText': '讀取到的某個值在郵件中找不到', + 'mailInbox.reason.imageOnly': '僅從圖片讀取,需要你確認', + 'mailInbox.reason.missingCriticalField': '缺少類型、日期或路線', + 'mailInbox.reason.missingConfirmation': '找不到預訂編號', + 'mailInbox.reason.tooManyBookings': '一封郵件中的預訂數量超過了自動加入的上限', + 'mailInbox.reason.changeOrCancellation': '這是對先前預訂的變更或取消', + 'mailInbox.reason.cancelled': '該預訂已取消', + 'mailInbox.reason.possibleDuplicate': '旅行中可能已有這筆預訂', + 'mailInbox.reason.changeAvailable': '旅行中已有這筆預訂,這封郵件會變更它', + 'mailInbox.reason.conflict': '旅行中已有這筆預訂,但詳細資料不同', + 'mailInbox.reason.askFirst': '你設定了逐筆確認預訂', + 'mailInbox.settings.title': '預訂郵件', + 'mailInbox.settings.on': '開', + 'mailInbox.settings.off': '關', + 'mailInbox.settings.yourAddress': '你的地址', + 'mailInbox.settings.copy': '複製地址', + 'mailInbox.settings.copyError': '無法複製', + 'mailInbox.settings.renew': '新地址', + 'mailInbox.settings.renewConfirm': '再點一次:舊地址將失效', + 'mailInbox.settings.renewed': '你的新地址已就緒', + 'mailInbox.settings.saveError': '無法儲存', + 'mailInbox.settings.addressHint': '把預訂確認郵件轉寄到此地址,TREK 會把它們加入正確的旅行中。請像密碼一樣妥善保管。', + 'mailInbox.settings.howTo': '自動轉寄', + 'mailInbox.settings.howToGmail': + 'Gmail:設定,轉寄和 POP/IMAP,新增轉寄地址。Gmail 會寄出一組驗證碼,它會顯示在「待處理的預訂」中。接著為預訂寄件者新增一個篩選器,轉寄到 TREK。', + 'mailInbox.settings.howToOutlook': + 'Outlook:設定,郵件,規則,新增一條轉寄預訂郵件的規則。部分工作帳戶會禁止自動轉寄,此時請手動轉寄。', + 'mailInbox.settings.howToManual': '手動:轉寄任何預訂郵件,包括附件。', + 'mailInbox.settings.enabled': '匯入預訂郵件', + 'mailInbox.settings.enabledHint': '關閉後,寄到你地址的郵件不會被讀取。', + 'mailInbox.settings.ai': '用 AI 讀取', + 'mailInbox.settings.aiHint': '讀取任何預訂郵件並找到對應的旅行。郵件以及你旅行的日期和目的地會傳送給 AI 服務商。', + 'mailInbox.settings.aiUnavailable': '此處無法使用:只會讀取包含機器可讀預訂資料的郵件。', + 'mailInbox.settings.autoAdd': '自動加入', + 'mailInbox.settings.autoAddHint': 'TREK 有把握的預訂會直接加入旅行,7 天內可以復原。關閉後,每筆預訂都會等你確認。', + 'mailInbox.settings.upload': '上傳郵件檔案', + 'mailInbox.settings.uploaded': '已收到郵件,正在讀取', + 'mailInbox.settings.uploadDuplicate': '這封郵件已經匯入過了', + 'mailInbox.settings.uploadError': '無法將該檔案讀取為郵件', + 'mailInbox.settings.privacy': '轉寄的郵件會加密保存最多 30 天,然後刪除。請不要轉寄身分證件或健康文件。', + 'mailInbox.trip.title': '或透過郵件轉寄', + 'mailInbox.trip.hint': '寄到此旅行專屬地址的預訂郵件會直接出現在這裡,無需猜測。', + 'mailInbox.trip.create': '為此旅行建立地址', + 'mailInbox.fromMail': '來自郵件', + 'mailInbox.fromMailHint': '由轉寄的預訂郵件加入', +}; +export default mailInbox; diff --git a/shared/src/i18n/zh-TW/notif.ts b/shared/src/i18n/zh-TW/notif.ts index b9a68d769..a22945341 100644 --- a/shared/src/i18n/zh-TW/notif.ts +++ b/shared/src/i18n/zh-TW/notif.ts @@ -58,7 +58,7 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} 已新增到 {trip}', 'notif.mail_booking_added.shared': '{actor} 從轉寄的郵件將 {booking} 新增到 {trip}', 'notif.mail_booking_pending.title': '待檢查的轉寄預訂', - 'notif.mail_booking_pending.text': '等待你處理的轉寄預訂:{count}', + 'notif.mail_booking_pending.text': '有 {count} 筆轉寄的預訂等你處理', 'notif.mail_ingest_paused.title': '郵件匯入已暫停', 'notif.mail_ingest_paused.text': '暫時不接收轉寄的預訂郵件。你的地址會保留,匯入恢復後即可再次使用。', 'notif.action.undo': '復原', diff --git a/shared/src/i18n/zh-TW/reservations.ts b/shared/src/i18n/zh-TW/reservations.ts index c56a32cd2..3703b992f 100644 --- a/shared/src/i18n/zh-TW/reservations.ts +++ b/shared/src/i18n/zh-TW/reservations.ts @@ -35,7 +35,10 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': '轉乘航班', 'reservations.layover.layover': '轉機等候', 'reservations.needsReview': '待確認', - 'reservations.needsReviewHint': '無法自動匹配機場 — 請確認位置。', + 'reservations.needsReviewHint': '部分資訊是自動填入的,可能沒有讀取準確。請檢查一下。', + 'reservations.reviewConfirm': '沒問題', + 'reservations.reviewConfirmHint': '點擊標記為已檢查。', + 'reservations.reviewConfirmed': '已標記為已檢查', 'reservations.searchLocation': '搜尋車站、港口、地址...', 'reservations.meta.trainNumber': '車次', 'reservations.meta.platform': '站臺', diff --git a/shared/src/i18n/zh-TW/settings.ts b/shared/src/i18n/zh-TW/settings.ts index f6aeac2f3..078663243 100644 --- a/shared/src/i18n/zh-TW/settings.ts +++ b/shared/src/i18n/zh-TW/settings.ts @@ -108,6 +108,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': '航班延誤', 'settings.notifyFlightLanded': '航班降落', 'settings.notifyTripReminder': '旅行提醒', + 'settings.notifyMailBookingAdded': '從郵件加入的預訂', + 'settings.notifyMailBookingPending': '待審核的轉寄預訂', + 'settings.notifyMailIngestPaused': '郵件匯入已暫停', 'settings.notifyTodoDue': '待辦事項即將到期', 'settings.notifyVacayInvite': 'Vacay 融合邀請', 'settings.notifyVacayShare': 'Vacay 日曆共享', diff --git a/shared/src/i18n/zh/index.ts b/shared/src/i18n/zh/index.ts index 50591f7a7..874da0dd7 100644 --- a/shared/src/i18n/zh/index.ts +++ b/shared/src/i18n/zh/index.ts @@ -36,6 +36,8 @@ import help from './help'; import inspector from './inspector'; import journey from './journey'; import login from './login'; +import mailAdmin from './mailAdmin'; +import mailInbox from './mailInbox'; import map from './map'; import members from './members'; import memories from './memories'; @@ -154,6 +156,8 @@ const locale = { ...docsync, ...ferratas, ...flightLive, + ...mailInbox, + ...mailAdmin, ...assistant, ...assistantCard, ...assistantTour, diff --git a/shared/src/i18n/zh/mailAdmin.ts b/shared/src/i18n/zh/mailAdmin.ts new file mode 100644 index 000000000..ae0fb60a8 --- /dev/null +++ b/shared/src/i18n/zh/mailAdmin.ts @@ -0,0 +1,45 @@ +import type { TranslationStrings } from '../types'; + +const mailAdmin: TranslationStrings = { + 'mailAdmin.title': '预订邮件导入', + 'mailAdmin.hint': '转发的预订确认邮件会变成正确旅行中的预订。', + 'mailAdmin.enabled': '导入预订邮件', + 'mailAdmin.enabledHint': '为此实例上的每个人提供一个转发地址。关闭后,任何地址都不接收邮件。', + 'mailAdmin.transport.label': '邮件送达方式', + 'mailAdmin.transport.off': '仅上传', + 'mailAdmin.transport.imap': 'IMAP 邮箱', + 'mailAdmin.transport.webhook': 'Webhook', + 'mailAdmin.aiDaily': '每人每天的 AI 邮件数', + 'mailAdmin.aiDailyHint': '超过后,只读取机器可读的预订数据。', + 'mailAdmin.template': '地址模板', + 'mailAdmin.templateHint': '转发地址,在每个人的验证码所在位置使用 {token}。', + 'mailAdmin.imap.host': '服务器', + 'mailAdmin.imap.port': '端口', + 'mailAdmin.imap.tls': '加密', + 'mailAdmin.imap.user': '用户', + 'mailAdmin.imap.pass': '密码', + 'mailAdmin.imap.mailbox': '文件夹', + 'mailAdmin.imap.mailboxHint': '已读取的邮件会移到 TREK/processed。', + 'mailAdmin.webhook.secret': '签名密钥', + 'mailAdmin.webhook.secretHint': '至少 32 个字符;发送方用它为每封邮件签名。留空会关闭 Webhook。', + 'mailAdmin.inboundUrl': 'Webhook 地址', + 'mailAdmin.inboundUrlHint': '你的邮件服务会把每封邮件以原始且已签名的形式发送到这里。', + 'mailAdmin.copyUrl': '复制地址', + 'mailAdmin.envHint': '环境变量(MAIL_INGEST_*)的优先级高于这些字段。', + 'mailAdmin.loadError': '无法加载邮件导入设置。', + 'mailAdmin.problem.secretShort': '密钥至少需要 32 个字符。', + 'mailAdmin.problem.portInvalid': '请输入 1 到 65535 之间的端口。', + 'mailAdmin.problem.templateToken': '地址中需要在每个人的验证码所在位置使用 {token}。', + 'mailAdmin.problem.dailyInvalid': '请输入 0 到 1000 之间的整数。', + 'mailAdmin.status.title': '状态', + 'mailAdmin.status.lastRead': '上次读取 {time}', + 'mailAdmin.status.never': '从未', + 'mailAdmin.status.mails': '邮件,24 小时', + 'mailAdmin.status.added': '自动添加', + 'mailAdmin.status.waiting': '等待中', + 'mailAdmin.status.failed': '失败', + 'mailAdmin.test.run': '测试连接', + 'mailAdmin.test.ok': '已连接', + 'mailAdmin.test.failed': '无法连接', +}; +export default mailAdmin; diff --git a/shared/src/i18n/zh/mailInbox.ts b/shared/src/i18n/zh/mailInbox.ts new file mode 100644 index 000000000..03b8d2689 --- /dev/null +++ b/shared/src/i18n/zh/mailInbox.ts @@ -0,0 +1,92 @@ +import type { TranslationStrings } from '../types'; + +const mailInbox: TranslationStrings = { + 'mailInbox.title': '待处理的预订', + 'mailInbox.pendingCount': '有 {count} 个预订等你处理', + 'mailInbox.empty': '没有待处理的内容', + 'mailInbox.emptyHint': 'TREK 无法自动添加的预订会显示在这里,并附上每一项可以怎么处理。', + 'mailInbox.untitled': '预订邮件', + 'mailInbox.showMail': '查看邮件', + 'mailInbox.backToList': '返回列表', + 'mailInbox.loadMore': '显示更多', + 'mailInbox.pickTrip': '哪个旅行?', + 'mailInbox.added': '已添加预订', + 'mailInbox.filed': '已保存到旅行的文件中', + 'mailInbox.discarded': '已丢弃', + 'mailInbox.actionError': '操作没有成功,请稍后再试。', + 'mailInbox.previewError': '无法显示这封邮件。', + 'mailInbox.action.add': '添加', + 'mailInbox.action.choose': '添加到旅行', + 'mailInbox.action.file': '保存为文档', + 'mailInbox.action.manual': '自己填写', + 'mailInbox.action.discard': '丢弃', + 'mailInbox.reason.forwardingConfirmation': '你的邮件服务商要求确认转发。请在那里输入此验证码。', + 'mailInbox.reason.newsletter': '这是新闻邮件,不是预订', + 'mailInbox.reason.autoReply': '这是自动回复', + 'mailInbox.reason.emptyMail': '邮件中没有可读取的正文或附件', + 'mailInbox.reason.mailUnreadable': '无法读取这封邮件', + 'mailInbox.reason.notABooking': '这封邮件中没有找到预订', + 'mailInbox.reason.tooLarge': '邮件太大,无法读取', + 'mailInbox.reason.addressInvalid': '邮件发到了已失效的地址', + 'mailInbox.reason.addressDailyLimit': '你的地址今天接收的邮件已达上限', + 'mailInbox.reason.processingFailed': '多次读取失败', + 'mailInbox.reason.aiQuotaExhausted': '本周期的 AI 额度已用完', + 'mailInbox.reason.aiDisabled': '未使用 AI 读取,没有找到预订', + 'mailInbox.reason.aiUserCap': '已达到你每天用 AI 读取邮件的上限', + 'mailInbox.reason.aiProviderError': 'AI 暂时无法读取', + 'mailInbox.reason.noTripInRange': '没有涵盖这些日期的旅行', + 'mailInbox.reason.tripAmbiguous': '可能对应不止一个旅行', + 'mailInbox.reason.tripWithoutDates': '旅行没有可供核对的日期', + 'mailInbox.reason.noEditPermission': '你无权向该旅行添加预订', + 'mailInbox.reason.demoMode': '演示模式不会添加预订', + 'mailInbox.reason.outsideTripDates': '有日期不在旅行范围内', + 'mailInbox.reason.sourceMismatch': '邮件中的信息互相矛盾', + 'mailInbox.reason.implausible': '有一个值不可能是正确的', + 'mailInbox.reason.notInText': '读取到的某个值在邮件中找不到', + 'mailInbox.reason.imageOnly': '仅从图片读取,需要你确认', + 'mailInbox.reason.missingCriticalField': '缺少类型、日期或路线', + 'mailInbox.reason.missingConfirmation': '没有找到预订编号', + 'mailInbox.reason.tooManyBookings': '一封邮件中的预订数量超过了自动添加的上限', + 'mailInbox.reason.changeOrCancellation': '这是对之前预订的更改或取消', + 'mailInbox.reason.cancelled': '该预订已取消', + 'mailInbox.reason.possibleDuplicate': '旅行中可能已有这个预订', + 'mailInbox.reason.changeAvailable': '旅行中已有这个预订,这封邮件会更改它', + 'mailInbox.reason.conflict': '旅行中已有这个预订,但详细信息不同', + 'mailInbox.reason.askFirst': '你设置了逐个确认预订', + 'mailInbox.settings.title': '预订邮件', + 'mailInbox.settings.on': '开', + 'mailInbox.settings.off': '关', + 'mailInbox.settings.yourAddress': '你的地址', + 'mailInbox.settings.copy': '复制地址', + 'mailInbox.settings.copyError': '无法复制', + 'mailInbox.settings.renew': '新地址', + 'mailInbox.settings.renewConfirm': '再点一次:旧地址将失效', + 'mailInbox.settings.renewed': '你的新地址已就绪', + 'mailInbox.settings.saveError': '无法保存', + 'mailInbox.settings.addressHint': + '把预订确认邮件转发到此地址,TREK 会把它们添加到正确的旅行中。请像密码一样妥善保管。', + 'mailInbox.settings.howTo': '自动转发', + 'mailInbox.settings.howToGmail': + 'Gmail:设置,转发和 POP/IMAP,添加转发地址。Gmail 会发送一个验证码,它会显示在“待处理的预订”中。然后为预订发件人添加一个过滤器,转发到 TREK。', + 'mailInbox.settings.howToOutlook': + 'Outlook:设置,邮件,规则,添加一条转发预订邮件的规则。部分工作账户会禁止自动转发,此时请手动转发。', + 'mailInbox.settings.howToManual': '手动:转发任何预订邮件,包括附件。', + 'mailInbox.settings.enabled': '导入预订邮件', + 'mailInbox.settings.enabledHint': '关闭后,发到你地址的邮件不会被读取。', + 'mailInbox.settings.ai': '用 AI 读取', + 'mailInbox.settings.aiHint': '读取任何预订邮件并找到对应的旅行。邮件以及你旅行的日期和目的地会发送给 AI 服务商。', + 'mailInbox.settings.aiUnavailable': '此处不可用:只会读取包含机器可读预订数据的邮件。', + 'mailInbox.settings.autoAdd': '自动添加', + 'mailInbox.settings.autoAddHint': 'TREK 有把握的预订会直接加入旅行,7 天内可以撤销。关闭后,每个预订都会等你确认。', + 'mailInbox.settings.upload': '上传邮件文件', + 'mailInbox.settings.uploaded': '已收到邮件,正在读取', + 'mailInbox.settings.uploadDuplicate': '这封邮件已经导入过了', + 'mailInbox.settings.uploadError': '无法将该文件读取为邮件', + 'mailInbox.settings.privacy': '转发的邮件会加密保存最多 30 天,然后删除。请不要转发身份证件或健康文件。', + 'mailInbox.trip.title': '或通过邮件转发', + 'mailInbox.trip.hint': '发到此旅行专属地址的预订邮件会直接出现在这里,无需猜测。', + 'mailInbox.trip.create': '为此旅行创建地址', + 'mailInbox.fromMail': '来自邮件', + 'mailInbox.fromMailHint': '由转发的预订邮件添加', +}; +export default mailInbox; diff --git a/shared/src/i18n/zh/notif.ts b/shared/src/i18n/zh/notif.ts index dd3d3e602..4fa68c2a1 100644 --- a/shared/src/i18n/zh/notif.ts +++ b/shared/src/i18n/zh/notif.ts @@ -58,7 +58,7 @@ const notif: TranslationStrings = { 'notif.mail_booking_added.text': '{booking} 已添加到 {trip}', 'notif.mail_booking_added.shared': '{actor} 从转发的邮件中将 {booking} 添加到 {trip}', 'notif.mail_booking_pending.title': '待查看的转发预订', - 'notif.mail_booking_pending.text': '等待你处理的转发预订:{count}', + 'notif.mail_booking_pending.text': '有 {count} 个转发的预订等你处理', 'notif.mail_ingest_paused.title': '邮件导入已暂停', 'notif.mail_ingest_paused.text': '暂时不接收转发的预订邮件。你的地址会保留,导入恢复后即可再次使用。', 'notif.action.undo': '撤销', diff --git a/shared/src/i18n/zh/reservations.ts b/shared/src/i18n/zh/reservations.ts index 32784eda8..3c1fb7779 100644 --- a/shared/src/i18n/zh/reservations.ts +++ b/shared/src/i18n/zh/reservations.ts @@ -35,7 +35,10 @@ const reservations: TranslationStrings = { 'reservations.layover.connection': '转机', 'reservations.layover.layover': '中转停留', 'reservations.needsReview': '待确认', - 'reservations.needsReviewHint': '无法自动匹配机场 — 请确认位置。', + 'reservations.needsReviewHint': '部分信息是自动填写的,可能没有读取准确。请检查一下。', + 'reservations.reviewConfirm': '没问题', + 'reservations.reviewConfirmHint': '点击标记为已检查。', + 'reservations.reviewConfirmed': '已标记为已检查', 'reservations.searchLocation': '搜索车站、港口、地址...', 'reservations.meta.trainNumber': '车次', 'reservations.meta.platform': '站台', diff --git a/shared/src/i18n/zh/settings.ts b/shared/src/i18n/zh/settings.ts index 9ed2da653..41c201aba 100644 --- a/shared/src/i18n/zh/settings.ts +++ b/shared/src/i18n/zh/settings.ts @@ -107,6 +107,9 @@ const settings: TranslationStrings = { 'settings.notifyFlightDelayed': '航班延误', 'settings.notifyFlightLanded': '航班降落', 'settings.notifyTripReminder': '旅行提醒', + 'settings.notifyMailBookingAdded': '从邮件添加的预订', + 'settings.notifyMailBookingPending': '待审核的转发预订', + 'settings.notifyMailIngestPaused': '邮件导入已暂停', 'settings.notifyTodoDue': '待办事项即将到期', 'settings.notifyVacayInvite': '假期合并邀请', 'settings.notifyVacayShare': '假期日历共享', diff --git a/shared/src/mail-ingest/mail-ingest.schema.spec.ts b/shared/src/mail-ingest/mail-ingest.schema.spec.ts index 3990d0124..9b2741b84 100644 --- a/shared/src/mail-ingest/mail-ingest.schema.spec.ts +++ b/shared/src/mail-ingest/mail-ingest.schema.spec.ts @@ -18,7 +18,6 @@ import { mailIngestMessageSchema, mailIngestMoveRequestSchema, mailIngestPrefsUpdateRequestSchema, - mailIngestReviewRequestSchema, mailIngestSettingsResponseSchema, mailIngestUndoConflictSchema, mailIngestUndoRequestSchema, @@ -176,13 +175,6 @@ describe('mail-ingest requests', () => { expect(mailIngestUndoConflictSchema.safeParse({ ...conflict, affects: [] }).success).toBe(false); }); - it('MAILC-023: "Looks right" names known fields or none', () => { - expect(mailIngestReviewRequestSchema.safeParse({}).success).toBe(true); - expect(mailIngestReviewRequestSchema.safeParse({ fields: ['seat', 'gate'] }).success).toBe(true); - expect(mailIngestReviewRequestSchema.safeParse({ fields: [] }).success).toBe(false); - expect(mailIngestReviewRequestSchema.safeParse({ fields: ['notes'] }).success).toBe(false); - }); - it('MAILC-024: the inbox query defaults to the open entries, a page of 50, and coerces the cursor', () => { expect(mailIngestInboxQuerySchema.parse({})).toEqual({ status: 'open', limit: 50 }); expect(mailIngestInboxQuerySchema.parse({ status: 'resolved', before: '40', limit: '10' })).toEqual({ diff --git a/shared/src/mail-ingest/mail-ingest.schema.ts b/shared/src/mail-ingest/mail-ingest.schema.ts index e467b559d..8b0379da0 100644 --- a/shared/src/mail-ingest/mail-ingest.schema.ts +++ b/shared/src/mail-ingest/mail-ingest.schema.ts @@ -275,22 +275,6 @@ export const mailIngestPrefillResponseSchema = z.object({ }); export type MailIngestPrefillResponse = z.infer; -/** - * POST /api/trips/:tripId/reservations/:id/mail-review ("Looks right"): clear - * the check marks of the named fields, or of all of them. - */ -export const mailIngestReviewRequestSchema = z.strictObject({ - fields: z.array(mailIngestFieldSchema).min(1).optional(), -}); -export type MailIngestReviewRequest = z.infer; - -export const mailIngestReviewResponseSchema = z.object({ - reservation_id: idSchema, - /** The fields still marked. */ - uncertain_fields: z.array(mailIngestFieldSchema), -}); -export type MailIngestReviewResponse = z.infer; - // ── Mail coming in ────────────────────────────────────────────────────────── /** POST /api/mail-ingest/upload: an .eml file read like a forwarded mail; a mail already in is not read twice. */ diff --git a/wiki/Booking-Mail-Import.md b/wiki/Booking-Mail-Import.md index 2c0de8551..75b7da074 100644 --- a/wiki/Booking-Mail-Import.md +++ b/wiki/Booking-Mail-Import.md @@ -23,7 +23,9 @@ A trip can also have its own address: **Import** in the trip's Bookings or Trans 3. It checks the result: the dates fit the trip, the values are plausible and stand in the mail, the booking is not in the trip already. 4. With **Add on its own** on and every check passed, it adds the booking and tells you so; **Undo** in that notice takes it back for 7 days. Fellow travellers of a shared trip are told about it too. -Anything TREK is not sure about waits under **Pending bookings**. +A booking with a detail TREK could not read for sure carries a **Review** mark. Check it in the booking and click the mark (**Looks right** on the phone) to take it off. + +Anything TREK is not sure about waits under **Pending bookings**. A mail the AI could not read because of a passing error is read again on its own; once the day's AI allowance is used up, the mail waits with **Fill in yourself** and **Save as a document**. ## Pending bookings @@ -32,7 +34,6 @@ Open them from the account menu (the number beside the entry counts them) or fro - **Add** or **Add to a trip** adds the booking. - **Save as a document** keeps the mail's attachments, or the mail itself, in the trip's files. - **Fill in yourself** opens the booking form in the trip with what the mail held. -- **Read with AI** tries again where the AI could not read the mail before (allowance used up, a passing error). - **Discard** drops it. **Show mail** shows the mail's text. A forwarding confirmation from Gmail or Outlook appears here too, with its code in large type.