feat(mail-ingest): tell every address holder once when the import stops

A plan without the import switches it off from outside the server, and an
admin can switch it off too; the plan promises a "mail import paused" note.
An hourly job sends mail_ingest_paused to each holder of a live address with
their own import on, notes the pause first, and forgets it once it is back.
This commit is contained in:
Maurice
2026-11-13 17:11:30 +01:00
parent 1d052f725f
commit 44f669cd0e
72 changed files with 429 additions and 3 deletions
@@ -94,6 +94,18 @@ export class MailIngestAddressesRepository extends TrekRepository<MailIngestAddr
return Number(result.numUpdatedRows);
}
/** The people who hold at least one address that is not revoked. */
async listHolders(): Promise<number[]> {
const rows = await this.db()
.selectFrom('mail_ingest_addresses')
.select('user_id')
.distinct()
.where('revoked_at', 'is', null)
.orderBy('user_id')
.execute();
return rows.map((row) => row.user_id);
}
/** Notes that a mail came in on the address. */
async touchUsed(id: number, at: DbTimestamp): Promise<void> {
await this.db().updateTable('mail_ingest_addresses').set({ last_used_at: at }).where('id', '=', id).execute();
@@ -4,8 +4,12 @@ import type { DbTimestamp } from '../types';
import type { AssertRowKeys } from './_shared/rows';
import { TrekRepository } from './_shared/trek-repository';
/** What reads mail: an IMAP box, the TREK Cloud relay, or the processing pipeline itself. */
type MailIngestCursorTransport = 'imap' | 'cloud' | 'pipeline';
/**
* What reads mail: an IMAP box, the TREK Cloud relay, or the processing
* pipeline itself; and the pause notice, whose good run is the pause it
* announced.
*/
type MailIngestCursorTransport = 'imap' | 'cloud' | 'pipeline' | 'notice';
/** One `mail_ingest_cursor` row as stored. */
interface MailIngestCursorRow {
@@ -102,6 +106,16 @@ export class MailIngestCursorRepository extends TrekRepository<MailIngestCursor>
.execute();
}
/** Forgets the last good run, as if the reader had never run. */
async forgetOk(transport: MailIngestCursorTransport, mailbox: string): Promise<void> {
await this.db()
.updateTable('mail_ingest_cursor')
.set({ last_ok_at: null })
.where('transport', '=', transport)
.where('mailbox', '=', mailbox)
.execute();
}
/** A failed run, for the admin's status card; the position stays where the last good run left it. */
async recordError(transport: MailIngestCursorTransport, mailbox: string, error: string): Promise<void> {
await this.db()
@@ -75,6 +75,19 @@ export class MailIngestNotifyService {
);
}
/** @txStandalone the import stopped; the person's address takes no mail until it is back. */
async importPaused(userId: number): Promise<void> {
await this.quietly(
this.notifications.send({
event: 'mail_ingest_paused',
actorId: null,
scope: 'user',
targetId: userId,
params: {},
}),
);
}
/** @txStandalone one note for the bookings of a mail that wait in the inbox. */
async bookingsPending(userId: number, pending: number): Promise<void> {
this.inboxChanged(userId, pending);
@@ -0,0 +1,74 @@
import { MailIngestAddresses } from '../../db/entities/MailIngestAddresses.entity';
import { MailIngestCursor } from '../../db/entities/MailIngestCursor.entity';
import type { MailIngestAddressesRepository } from '../../db/repositories/MailIngestAddresses.repository';
import type { MailIngestCursorRepository } from '../../db/repositories/MailIngestCursor.repository';
import { dbNow } from '../../db/types';
import { logError } from '../audit/audit-log.logger';
import { CronRegistrarService, LEASE_OWNER } from '../scheduling/cron-registrar.service';
import { MailIngestNotifyService } from './mail-ingest-notify.service';
import { MailIngestSettingsService } from './mail-ingest-settings.service';
import { InjectRepository } from '@mikro-orm/nestjs';
import { Injectable, type OnApplicationBootstrap } from '@nestjs/common';
/** How long a run holds the notice before another process may take it over. */
const LEASE_MS = 10 * 60 * 1000;
/** The notice's row in the readers' cursor table: transport `notice`, this as its mailbox. */
const PAUSED = 'paused';
/**
* Tells every person who holds a forwarding address, once, that the import
* stopped: on the TREK Cloud a plan without it switches it off from outside
* the server, and an admin can switch it off in the dialog. Checked every
* hour. The notice's cursor row notes the pause it announced, and forgets it
* once the import is back, so the next pause is announced again. A person
* who switched their own import off hears nothing, having nothing to lose.
*/
@Injectable()
export class MailIngestPauseJob implements OnApplicationBootstrap {
constructor(
@InjectRepository(MailIngestCursor) private readonly cursor: MailIngestCursorRepository,
@InjectRepository(MailIngestAddresses) private readonly addresses: MailIngestAddressesRepository,
private readonly settings: MailIngestSettingsService,
private readonly notify: MailIngestNotifyService,
private readonly registrar: CronRegistrarService,
) {}
onApplicationBootstrap(): void {
if (!this.registrar.isEnabled()) return;
this.registrar.register('mail-ingest-pause-notice', '25 * * * *', () => this.tick());
}
/**
* One run; answers how many people it told.
*
* @txIndependent the pause is noted before anyone is told, so a run that
* dies halfway never tells anyone twice; each notice stands on its own.
*/
async tick(now: Date = new Date()): Promise<number> {
if (await this.settings.isAvailable()) {
if ((await this.cursor.find('notice', PAUSED))?.last_ok_at) await this.cursor.forgetOk('notice', PAUSED);
return 0;
}
const until = dbNow(new Date(now.getTime() + LEASE_MS));
if (!(await this.cursor.acquireLease('notice', PAUSED, LEASE_OWNER, until, dbNow(now)))) return 0;
try {
if ((await this.cursor.find('notice', PAUSED))?.last_ok_at) return 0;
const people = await this.peopleToTell();
await this.cursor.recordOk('notice', PAUSED, dbNow(now));
await Promise.all(people.map((userId) => this.notify.importPaused(userId)));
return people.length;
} catch (err) {
logError(`[mail-ingest] The pause notice failed: ${err instanceof Error ? err.message : String(err)}`);
return 0;
} finally {
await this.cursor.releaseLease('notice', PAUSED, LEASE_OWNER);
}
}
/** The holders of a live address who have their own import on. */
private async peopleToTell(): Promise<number[]> {
const holders = await this.addresses.listHolders();
const enabled = await Promise.all(holders.map(async (userId) => (await this.settings.prefsOf(userId)).enabled));
return holders.filter((_, i) => enabled[i]);
}
}
@@ -30,6 +30,7 @@ import { MailIngestInboxService } from './mail-ingest-inbox.service';
import { MailIngestIntakeService } from './mail-ingest-intake.service';
import { MailIngestNotificationActions } from './mail-ingest-notification-actions';
import { MailIngestNotifyService } from './mail-ingest-notify.service';
import { MailIngestPauseJob } from './mail-ingest-pause.job';
import { MailIngestProcessJob } from './mail-ingest-process.job';
import { MailIngestProcessService } from './mail-ingest-process.service';
import { MailIngestReadingService } from './mail-ingest-reading.service';
@@ -109,6 +110,7 @@ import { Module } from '@nestjs/common';
MailIngestRetentionJob,
MailIngestImapJob,
MailIngestRelayJob,
MailIngestPauseJob,
],
exports: [
MailIngestSettingsService,
@@ -48,6 +48,7 @@ export type NotifEventType =
| 'wanderer_source_hint'
| 'mail_booking_added'
| 'mail_booking_pending'
| 'mail_ingest_paused'
| 'plugin_notification';
/** Every event, in the order the preferences UI lists them. */
@@ -72,6 +73,7 @@ export const ALL_EVENT_TYPES: NotifEventType[] = [
'wanderer_source_hint',
'mail_booking_added',
'mail_booking_pending',
'mail_ingest_paused',
'plugin_notification',
];
@@ -137,6 +137,7 @@ export class NotificationPreferencesService {
return (await resolveAppSetting(this.appSettings, 'flight_live_enabled')) !== 'false';
case 'mail_booking_added':
case 'mail_booking_pending':
case 'mail_ingest_paused':
return (await resolveAppSetting(this.appSettings, 'mail_ingest_enabled')) === 'true';
default:
return true;
@@ -306,6 +306,14 @@ const EVENT_NOTIFICATION_CONFIG: Record<string, EventNotifConfig> = {
navigateTextKey: 'notif.action.review',
navigateTarget: () => '/dashboard?inbox=mail',
},
// The import stopped (a plan without it, or an admin switched it off): every
// person with an address hears it once, with nothing to open.
mail_ingest_paused: {
inAppType: 'simple',
titleKey: 'notif.mail_ingest_paused.title',
textKey: 'notif.mail_ingest_paused.text',
navigateTarget: () => null,
},
synology_session_cleared: {
inAppType: 'simple',
titleKey: 'notifications.synologySessionCleared.title',
@@ -41,6 +41,7 @@ const NTFY_EVENT_META: Partial<Record<NotifEventType, { priority: 1 | 2 | 3 | 4
wanderer_source_hint: { priority: 2, tags: ['hiking_boot'] },
mail_booking_added: { priority: 3, tags: ['envelope'] },
mail_booking_pending: { priority: 2, tags: ['envelope'] },
mail_ingest_paused: { priority: 3, tags: ['pause_button'] },
};
const NTFY_DEFAULT_META = { priority: 3 as const, tags: [] as string[] };
@@ -1,7 +1,8 @@
/**
* The forwarding addresses through their repository: a person's own address
* and a trip's are told apart, renewing revokes instead of deleting so the old
* token is still recognised, and a mail notes the address it came in on.
* token is still recognised, a mail notes the address it came in on, and the
* people who hold a live address are named for the pause notice.
*/
import { MailIngestAddresses } from '../../../../src/db/entities/MailIngestAddresses.entity';
import type { MailIngestAddressesRepository } from '../../../../src/db/repositories/MailIngestAddresses.repository';
@@ -93,4 +94,17 @@ describe('MailIngestAddressesRepository', () => {
await addresses.touchUsed(id, LATER);
expect((await addresses.findById(id))?.last_used_at).toBe(LATER);
});
it('names each person who holds an address that is not revoked, once', async () => {
const { user: mia } = await makeUser(t);
const { user: noah } = await makeUser(t);
const trip = await makeTrip(t, mia.id);
await addresses.insertAddress({ userId: mia.id, tripId: null, tokenHash: 'm1', tokenEnc: 'e', createdAt: AT });
await addresses.insertAddress({ userId: mia.id, tripId: trip.id, tokenHash: 'm2', tokenEnc: 'e', createdAt: AT });
await addresses.insertAddress({ userId: noah.id, tripId: null, tokenHash: 'n1', tokenEnc: 'e', createdAt: AT });
expect(await addresses.listHolders()).toEqual([mia.id, noah.id].sort((a, b) => a - b));
await addresses.revokeLive(noah.id, null, LATER);
expect(await addresses.listHolders()).toEqual([mia.id]);
});
});
@@ -58,6 +58,9 @@ describe('MailIngestCursorRepository', () => {
await cursor.recordOk('imap', 'INBOX', T1);
row = await cursor.find('imap', 'INBOX');
expect(row).toMatchObject({ uid_validity: 7, last_uid: 42, last_ok_at: T1, last_error: null });
await cursor.forgetOk('imap', 'INBOX');
expect(await cursor.find('imap', 'INBOX')).toMatchObject({ uid_validity: 7, last_ok_at: null });
expect(await cursor.find('cloud', '')).toBeNull();
});
});
@@ -74,6 +74,18 @@ describe('MailIngestNotifyService', () => {
notify.settingsChanged(3);
expect(realtime.broadcastToUser).toHaveBeenCalledWith(3, { type: 'mailInbox:settings' });
});
it('tells a person the import paused, and survives a failed send', async () => {
notifications.send.mockRejectedValue(new Error('smtp down'));
await expect(notify.importPaused(4)).resolves.toBeUndefined();
expect(notifications.send).toHaveBeenCalledWith({
event: 'mail_ingest_paused',
actorId: null,
scope: 'user',
targetId: 4,
params: {},
});
});
});
describe('MailIngestNotificationActions', () => {
@@ -0,0 +1,98 @@
/**
* The pause notice over stubs: when the import stops, every holder of a live
* address with their own import on hears it once; the pause is noted before
* anyone is told, a second run stays quiet, another process holding the
* notice is left alone, and the import coming back forgets the pause so the
* next one is announced again.
*/
import { MailIngestAddresses } from '../../../../src/db/entities/MailIngestAddresses.entity';
import { MailIngestCursor } from '../../../../src/db/entities/MailIngestCursor.entity';
import { MailIngestNotifyService } from '../../../../src/nest/mail-ingest/mail-ingest-notify.service';
import { MailIngestPauseJob } from '../../../../src/nest/mail-ingest/mail-ingest-pause.job';
import { MailIngestSettingsService } from '../../../../src/nest/mail-ingest/mail-ingest-settings.service';
import { CronRegistrarService } from '../../../../src/nest/scheduling/cron-registrar.service';
import { getRepositoryToken } from '@mikro-orm/nestjs';
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../../../../src/nest/audit/audit-log.logger', () => ({
LOG_LEVEL: 'error',
logInfo: vi.fn(),
logDebug: vi.fn(),
logError: vi.fn(),
logWarn: vi.fn(),
}));
const NOW = new Date('2030-01-15T10:00:00Z');
const settings = {
isAvailable: vi.fn(async () => false),
prefsOf: vi.fn(async (userId: number) => ({ enabled: userId !== 3, ai_enabled: true, auto_add: true })),
};
const cursor = {
find: vi.fn(),
acquireLease: vi.fn(async () => true),
releaseLease: vi.fn(async () => undefined),
recordOk: vi.fn(async () => undefined),
forgetOk: vi.fn(async () => undefined),
};
const addresses = { listHolders: vi.fn(async () => [1, 2, 3]) };
const notify = { importPaused: vi.fn(async () => undefined) };
const registrar = { isEnabled: () => false, register: vi.fn() };
let job: MailIngestPauseJob;
beforeEach(async () => {
vi.clearAllMocks();
settings.isAvailable.mockResolvedValue(false);
cursor.find.mockResolvedValue(null);
cursor.acquireLease.mockResolvedValue(true);
const moduleRef = await Test.createTestingModule({
providers: [
MailIngestPauseJob,
{ provide: MailIngestSettingsService, useValue: settings },
{ provide: MailIngestNotifyService, useValue: notify },
{ provide: CronRegistrarService, useValue: registrar },
{ provide: getRepositoryToken(MailIngestCursor), useValue: cursor },
{ provide: getRepositoryToken(MailIngestAddresses), useValue: addresses },
],
}).compile();
job = moduleRef.get(MailIngestPauseJob);
});
describe('the pause notice', () => {
it('tells each holder of a live address with their import on, once, after noting the pause', async () => {
expect(await job.tick(NOW)).toBe(2);
expect(notify.importPaused.mock.calls).toEqual([[1], [2]]);
expect(cursor.recordOk).toHaveBeenCalledWith('notice', 'paused', '2030-01-15 10:00:00');
expect(cursor.recordOk.mock.invocationCallOrder[0]).toBeLessThan(notify.importPaused.mock.invocationCallOrder[0]);
expect(cursor.releaseLease).toHaveBeenCalled();
cursor.find.mockResolvedValue({ last_ok_at: '2030-01-15 10:00:00' });
expect(await job.tick(NOW)).toBe(0);
expect(notify.importPaused).toHaveBeenCalledTimes(2);
});
it('leaves the notice to the process that holds it', async () => {
cursor.acquireLease.mockResolvedValue(false);
expect(await job.tick(NOW)).toBe(0);
expect(addresses.listHolders).not.toHaveBeenCalled();
});
it('forgets the pause once the import is back, and tells nobody while it runs', async () => {
settings.isAvailable.mockResolvedValue(true);
expect(await job.tick(NOW)).toBe(0);
expect(cursor.forgetOk).not.toHaveBeenCalled();
cursor.find.mockResolvedValue({ last_ok_at: '2030-01-15 10:00:00' });
expect(await job.tick(NOW)).toBe(0);
expect(cursor.forgetOk).toHaveBeenCalledWith('notice', 'paused');
expect(notify.importPaused).not.toHaveBeenCalled();
expect(cursor.acquireLease).not.toHaveBeenCalled();
});
it('logs a failed run and gives the lease back', async () => {
addresses.listHolders.mockRejectedValueOnce(new Error('db gone'));
expect(await job.tick(NOW)).toBe(0);
expect(cursor.recordOk).not.toHaveBeenCalled();
expect(cursor.releaseLease).toHaveBeenCalledWith('notice', 'paused', expect.any(String));
});
});
@@ -501,5 +501,6 @@ describe('the mail import’s events follow its switch', () => {
const { event_types } = await svc.getPreferencesMatrix(user.id, 'user');
expect(event_types).toContain('mail_booking_added');
expect(event_types).toContain('mail_booking_pending');
expect(event_types).toContain('mail_ingest_paused');
});
});
@@ -652,6 +652,7 @@ describe('send() — in-app notification content', () => {
targetId: user.id,
params: { count: '2' },
});
await send({ event: 'mail_ingest_paused', actorId: null, scope: 'user', targetId: user.id, params: {} });
const notifs = await getInAppNotifications(user.id);
expect(notifs.map((n) => [n.text_key, n.navigate_target])).toEqual(
@@ -659,6 +660,7 @@ describe('send() — in-app notification content', () => {
['notif.mail_booking_added.text', '/trips/7?tab=bookings'],
['notif.mail_booking_added.shared', '/trips/7?tab=bookings'],
['notif.mail_booking_pending.text', '/dashboard?inbox=mail'],
['notif.mail_ingest_paused.text', null],
]),
);
});
@@ -122,6 +122,10 @@ const ar: NotificationLocale = {
title: 'حجوزات مُعاد توجيهها للمراجعة',
body: `حجوزات مُعاد توجيهها بانتظارك في TREK: ${p.count}. يذكر كل منها سبب عدم إضافته تلقائيًا.`,
}),
mail_ingest_paused: () => ({
title: 'تم إيقاف استيراد البريد مؤقتًا',
body: 'لا يقبل TREK رسائل الحجز المُعاد توجيهها في الوقت الحالي، لذا انتظر قبل إعادة توجيهها. تبقى عناوينك وستعمل مجددًا عند عودة الاستيراد، وتبقى الحجوزات المضافة بالفعل في رحلاتك.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -64,6 +64,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': 'أضاف {actor} الحجز {booking} إلى {trip} من بريد مُعاد توجيهه',
'notif.mail_booking_pending.title': 'حجوزات مُعاد توجيهها للمراجعة',
'notif.mail_booking_pending.text': 'حجوزات مُعاد توجيهها بانتظارك: {count}',
'notif.mail_ingest_paused.title': 'تم إيقاف استيراد البريد مؤقتًا',
'notif.mail_ingest_paused.text': 'لا تُقبل رسائل الحجز المُعاد توجيهها في الوقت الحالي. تبقى عناوينك وستعمل مجددًا عند عودة الاستيراد.',
'notif.action.undo': 'تراجع',
'notif.action.keep': 'إبقاء',
'notif.action.review': 'مراجعة',
@@ -107,6 +107,10 @@ const az: NotificationLocale = {
title: 'Yoxlanılacaq yönləndirilmiş rezervasiyalar',
body: `TREK-də sizi gözləyən yönləndirilmiş rezervasiyalar: ${p.count}. Hər birində niyə avtomatik əlavə edilmədiyi yazılıb.`,
}),
mail_ingest_paused: () => ({
title: 'Poçt idxalı dayandırılıb',
body: 'TREK hələlik yönləndirilmiş rezervasiya məktublarını qəbul etmir, ona görə də onları hələ yönləndirməyin. Ünvanlarınız qalır və idxal geri qayıdanda yenidən işləyəcək, artıq əlavə edilmiş rezervasiyalar isə səfərlərinizdə qalır.',
}),
plugin_notification: (p) => ({
title: p.title ?? '',
body: p.body ?? '',
+2
View File
@@ -61,6 +61,8 @@ const notif: TranslationStrings = {
'{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_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.',
'notif.action.undo': 'Geri al',
'notif.action.keep': 'Saxla',
'notif.action.review': 'Yoxla',
@@ -110,6 +110,10 @@ const br: NotificationLocale = {
title: 'Reservas encaminhadas para revisar',
body: `Reservas encaminhadas aguardando você no TREK: ${p.count}. Cada uma diz por que não foi adicionada sozinha.`,
}),
mail_ingest_paused: () => ({
title: 'Importação de e-mails pausada',
body: 'Por enquanto, o TREK não aceita e-mails de reserva encaminhados, então espere antes de encaminhá-los. Seus endereços continuam e voltam a funcionar quando a importação voltar, e as reservas já adicionadas continuam nas suas viagens.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Desfazer',
'notif.action.keep': 'Manter',
'notif.action.review': 'Revisar',
@@ -107,6 +107,10 @@ const ca: NotificationLocale = {
title: 'Reserves reenviades per revisar',
body: `Reserves reenviades pendents a TREK: ${p.count}. Cadascuna diu per què no s’ha afegit sola.`,
}),
mail_ingest_paused: () => ({
title: 'Importació de correus en pausa',
body: 'De moment TREK no accepta correus de reserva reenviats, així que espera abans de reenviar-ne. Les teves adreces es mantenen i tornaran a funcionar quan torni la importació, i les reserves ja afegides continuen als teus viatges.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -65,6 +65,8 @@ const notif: TranslationStrings = {
'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_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ó.',
'notif.action.undo': 'Desfés',
'notif.action.keep': 'Conserva',
'notif.action.review': 'Revisa',
@@ -113,6 +113,10 @@ const cs: NotificationLocale = {
title: 'Přeposlané rezervace ke kontrole',
body: `Přeposlané rezervace čekající na vás v TREK: ${p.count}. U každé je uvedeno, proč nebyla přidána sama.`,
}),
mail_ingest_paused: () => ({
title: 'Import e-mailů pozastaven',
body: 'TREK zatím nepřijímá přeposlané e-maily s rezervacemi, proto je zatím nepřeposílejte. Vaše adresy zůstávají a znovu budou fungovat, až se import vrátí, a již přidané rezervace zůstávají ve vašich cestách.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -61,6 +61,8 @@ const notif: TranslationStrings = {
'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_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í.',
'notif.action.undo': 'Vrátit zpět',
'notif.action.keep': 'Ponechat',
'notif.action.review': 'Zkontrolovat',
@@ -107,6 +107,10 @@ const de: NotificationLocale = {
title: 'Weitergeleitete Buchungen prüfen',
body: `In TREK warten weitergeleitete Buchungen auf dich: ${p.count}. Bei jeder steht, warum sie nicht von selbst eingetragen wurde.`,
}),
mail_ingest_paused: () => ({
title: 'Mail-Import pausiert',
body: 'TREK nimmt weitergeleitete Buchungsmails vorerst nicht an, leite also gerade keine weiter. Deine Adressen bleiben und funktionieren wieder, sobald der Import zurück ist, und bereits eingetragene Buchungen bleiben in deinen Reisen.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Rückgängig',
'notif.action.keep': 'Behalten',
'notif.action.review': 'Prüfen',
@@ -107,6 +107,10 @@ const en: NotificationLocale = {
title: 'Forwarded bookings to review',
body: `Forwarded bookings waiting for you in TREK: ${p.count}. Each one says why it was not added on its own.`,
}),
mail_ingest_paused: () => ({
title: 'Mail import paused',
body: 'TREK does not take in forwarded booking mails for now, so hold off forwarding them. Your addresses stay and work again once the import is back, and bookings already added stay in your trips.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Undo',
'notif.action.keep': 'Keep',
'notif.action.review': 'Review',
@@ -107,6 +107,10 @@ const es: NotificationLocale = {
title: 'Reservas reenviadas por revisar',
body: `Reservas reenviadas pendientes en TREK: ${p.count}. Cada una indica por qué no se añadió sola.`,
}),
mail_ingest_paused: () => ({
title: 'Importación de correos en pausa',
body: 'Por ahora TREK no acepta correos de reserva reenviados, así que espera antes de reenviarlos. Tus direcciones se mantienen y volverán a funcionar cuando vuelva la importación, y las reservas ya añadidas siguen en tus viajes.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Deshacer',
'notif.action.keep': 'Mantener',
'notif.action.review': 'Revisar',
@@ -108,6 +108,10 @@ const et: NotificationLocale = {
title: 'Ülevaatamist ootavad edastatud broneeringud',
body: `TREKis ootavad sind edastatud broneeringud: ${p.count}. Igaühe juures on kirjas, miks seda ise ei lisatud.`,
}),
mail_ingest_paused: () => ({
title: 'Kirjade import on peatatud',
body: 'TREK ei võta edastatud broneeringukirju praegu vastu, seega oota nende edastamisega. Sinu aadressid jäävad alles ja töötavad uuesti, kui import tagasi on, ning juba lisatud broneeringud jäävad sinu reisidesse.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Võta tagasi',
'notif.action.keep': 'Jäta alles',
'notif.action.review': 'Vaata üle',
@@ -42,6 +42,7 @@ export type NotificationEventKey =
| 'wanderer_source_hint'
| 'mail_booking_added'
| 'mail_booking_pending'
| 'mail_ingest_paused'
| 'plugin_notification';
export interface NotificationLocale {
@@ -107,6 +107,10 @@ const fr: NotificationLocale = {
title: 'Réservations transférées à vérifier',
body: `Réservations transférées en attente dans TREK : ${p.count}. Chacune indique pourquoi elle n’a pas été ajoutée automatiquement.`,
}),
mail_ingest_paused: () => ({
title: 'Import des e-mails en pause',
body: 'TREK n’accepte pas les e-mails de réservation transférés pour le moment, attendez donc avant d’en transférer. Vos adresses restent et fonctionneront de nouveau au retour de l’import, et les réservations déjà ajoutées restent dans vos voyages.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Annuler',
'notif.action.keep': 'Garder',
'notif.action.review': 'Vérifier',
@@ -110,6 +110,10 @@ const gr: NotificationLocale = {
title: 'Προωθημένες κρατήσεις για έλεγχο',
body: `Προωθημένες κρατήσεις σε αναμονή στο TREK: ${p.count}. Σε καθεμία αναφέρεται γιατί δεν προστέθηκε αυτόματα.`,
}),
mail_ingest_paused: () => ({
title: 'Η εισαγωγή email σε παύση',
body: 'Το TREK δεν δέχεται προς το παρόν προωθημένα email κρατήσεων, οπότε περίμενε πριν τα προωθήσεις. Οι διευθύνσεις σου μένουν και θα λειτουργήσουν ξανά όταν επιστρέψει η εισαγωγή, και οι κρατήσεις που έχουν ήδη προστεθεί μένουν στα ταξίδια σου.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': 'Ο/Η {actor} πρόσθεσε την κράτηση {booking} στο {trip} από προωθημένο email',
'notif.mail_booking_pending.title': 'Προωθημένες κρατήσεις για έλεγχο',
'notif.mail_booking_pending.text': 'Προωθημένες κρατήσεις σε αναμονή: {count}',
'notif.mail_ingest_paused.title': 'Η εισαγωγή email σε παύση',
'notif.mail_ingest_paused.text': 'Τα προωθημένα email κρατήσεων δεν γίνονται δεκτά προς το παρόν. Οι διευθύνσεις σου μένουν και θα λειτουργήσουν ξανά όταν επιστρέψει η εισαγωγή.',
'notif.action.undo': 'Αναίρεση',
'notif.action.keep': 'Διατήρηση',
'notif.action.review': 'Έλεγχος',
@@ -113,6 +113,10 @@ const he: NotificationLocale = {
title: 'הזמנות שהועברו לבדיקה',
body: `הזמנות שהועברו וממתינות לכם ב-TREK: ${p.count}. ליד כל אחת כתוב מדוע לא נוספה אוטומטית.`,
}),
mail_ingest_paused: () => ({
title: 'ייבוא הדואר מושהה',
body: 'TREK אינו מקבל כרגע הודעות הזמנה שהועברו, לכן המתינו לפני שתעבירו אותן. הכתובות שלכם נשמרות ויעבדו שוב כשהייבוא יחזור, והזמנות שכבר נוספו נשארות בטיולים שלכם.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -61,6 +61,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} הוסיף/ה את {booking} אל {trip} מדוא״ל שהועבר',
'notif.mail_booking_pending.title': 'הזמנות שהועברו לבדיקה',
'notif.mail_booking_pending.text': 'הזמנות שהועברו וממתינות לכם: {count}',
'notif.mail_ingest_paused.title': 'ייבוא הדואר מושהה',
'notif.mail_ingest_paused.text': 'הודעות הזמנה שהועברו אינן מתקבלות כרגע. הכתובות שלכם נשמרות ויעבדו שוב כשהייבוא יחזור.',
'notif.action.undo': 'ביטול',
'notif.action.keep': 'השארה',
'notif.action.review': 'בדיקה',
@@ -107,6 +107,10 @@ const hu: NotificationLocale = {
title: 'Ellenőrizendő továbbított foglalások',
body: `Rád váró továbbított foglalások a TREK-ben: ${p.count}. Mindegyiknél ott áll, miért nem került be magától.`,
}),
mail_ingest_paused: () => ({
title: 'E-mail-importálás szüneteltetve',
body: 'A TREK egyelőre nem fogad továbbított foglalási e-maileket, ezért most ne továbbíts ilyet. A címeid megmaradnak, és újra működnek, amint az importálás visszatér, a már hozzáadott foglalások pedig az utazásaidban maradnak.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Visszavonás',
'notif.action.keep': 'Megtartás',
'notif.action.review': 'Ellenőrzés',
@@ -98,6 +98,10 @@ const id: NotificationLocale = {
title: 'Pemesanan terusan untuk ditinjau',
body: `Pemesanan terusan yang menunggumu di TREK: ${p.count}. Masing-masing menyebutkan mengapa tidak ditambahkan otomatis.`,
}),
mail_ingest_paused: () => ({
title: 'Impor email dijeda',
body: 'TREK untuk sementara tidak menerima email pemesanan yang diteruskan, jadi tunda dulu meneruskannya. Alamatmu tetap ada dan akan berfungsi lagi saat impor kembali, dan pemesanan yang sudah ditambahkan tetap ada di perjalananmu.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -59,6 +59,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Urungkan',
'notif.action.keep': 'Simpan',
'notif.action.review': 'Tinjau',
@@ -104,6 +104,10 @@ const it: NotificationLocale = {
title: 'Prenotazioni inoltrate da controllare',
body: `Prenotazioni inoltrate in attesa in TREK: ${p.count}. Ognuna indica perché non è stata aggiunta da sola.`,
}),
mail_ingest_paused: () => ({
title: 'Importazione email in pausa',
body: 'Per ora TREK non accetta le email di prenotazione inoltrate, quindi aspetta prima di inoltrarle. I tuoi indirizzi restano e funzioneranno di nuovo quando l’importazione tornerà, e le prenotazioni già aggiunte restano nei tuoi viaggi.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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à.',
'notif.action.undo': 'Annulla',
'notif.action.keep': 'Mantieni',
'notif.action.review': 'Controlla',
@@ -98,6 +98,10 @@ const ja: NotificationLocale = {
title: '確認待ちの転送予約',
body: `TREK に確認待ちの転送予約があります: ${p.count} 件。それぞれに自動で追加されなかった理由が表示されます。`,
}),
mail_ingest_paused: () => ({
title: 'メール取り込みを一時停止中',
body: 'TREK は現在、転送された予約メールを受け付けていないため、転送はしばらくお待ちください。アドレスはそのまま残り、取り込みが再開されると再び使えます。すでに追加された予約は旅行に残ります。',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -59,6 +59,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} が転送メールから {booking} を {trip} に追加しました',
'notif.mail_booking_pending.title': '確認待ちの転送予約',
'notif.mail_booking_pending.text': '確認待ちの転送予約: {count} 件',
'notif.mail_ingest_paused.title': 'メール取り込みを一時停止中',
'notif.mail_ingest_paused.text': '転送された予約メールは現在受け付けていません。アドレスはそのまま残り、取り込みが再開されると再び使えます。',
'notif.action.undo': '元に戻す',
'notif.action.keep': '残す',
'notif.action.review': '確認',
@@ -98,6 +98,10 @@ const ko: NotificationLocale = {
title: '검토할 전달된 예약',
body: `TREK에서 검토를 기다리는 전달된 예약: ${p.count}개. 각 예약에 자동으로 추가되지 않은 이유가 표시됩니다.`,
}),
mail_ingest_paused: () => ({
title: '메일 가져오기 일시 중지됨',
body: 'TREK은 당분간 전달된 예약 메일을 받지 않으니 전달을 잠시 미뤄 주세요. 주소는 그대로 유지되며 가져오기가 다시 시작되면 다시 작동하고, 이미 추가된 예약은 여행에 그대로 남습니다.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -59,6 +59,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} 님이 전달된 메일에서 {booking}을(를) {trip}에 추가했습니다',
'notif.mail_booking_pending.title': '검토할 전달된 예약',
'notif.mail_booking_pending.text': '검토를 기다리는 전달된 예약: {count}개',
'notif.mail_ingest_paused.title': '메일 가져오기 일시 중지됨',
'notif.mail_ingest_paused.text': '전달된 예약 메일은 당분간 받지 않습니다. 주소는 그대로 유지되며 가져오기가 다시 시작되면 다시 작동합니다.',
'notif.action.undo': '실행 취소',
'notif.action.keep': '유지',
'notif.action.review': '검토',
@@ -107,6 +107,10 @@ const nl: NotificationLocale = {
title: 'Doorgestuurde boekingen om te controleren',
body: `In TREK wachten doorgestuurde boekingen op je: ${p.count}. Bij elke staat waarom hij niet vanzelf is toegevoegd.`,
}),
mail_ingest_paused: () => ({
title: 'Mailimport gepauzeerd',
body: 'TREK neemt doorgestuurde boekingsmails voorlopig niet aan, stuur er dus even geen door. Je adressen blijven en werken weer zodra de import terug is, en boekingen die al zijn toegevoegd blijven in je reizen.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Ongedaan maken',
'notif.action.keep': 'Behouden',
'notif.action.review': 'Controleren',
@@ -116,6 +116,10 @@ const pl: NotificationLocale = {
title: 'Przekazane rezerwacje do sprawdzenia',
body: `Przekazane rezerwacje czekające na Ciebie w TREK: ${p.count}. Przy każdej podano, dlaczego nie została dodana automatycznie.`,
}),
mail_ingest_paused: () => ({
title: 'Import e-maili wstrzymany',
body: 'TREK na razie nie przyjmuje przekazanych e-maili z rezerwacjami, więc wstrzymaj się z ich przekazywaniem. Twoje adresy zostają i znów zadziałają, gdy import wróci, a dodane już rezerwacje zostają w Twoich podróżach.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -62,6 +62,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Cofnij',
'notif.action.keep': 'Zachowaj',
'notif.action.review': 'Sprawdź',
@@ -111,6 +111,10 @@ const ru: NotificationLocale = {
title: 'Пересланные бронирования для проверки',
body: `Пересланных бронирований ждёт вас в TREK: ${p.count}. У каждого указано, почему оно не добавлено автоматически.`,
}),
mail_ingest_paused: () => ({
title: 'Импорт писем приостановлен',
body: 'TREK пока не принимает пересланные письма с бронированиями, поэтому пока не пересылайте их. Ваши адреса сохраняются и снова заработают, когда импорт вернётся, а уже добавленные бронирования остаются в ваших поездках.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -62,6 +62,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} добавил(а) {booking} в {trip} из пересланного письма',
'notif.mail_booking_pending.title': 'Пересланные бронирования для проверки',
'notif.mail_booking_pending.text': 'Пересланных бронирований ждёт вас: {count}',
'notif.mail_ingest_paused.title': 'Импорт писем приостановлен',
'notif.mail_ingest_paused.text': 'Пересланные письма с бронированиями пока не принимаются. Ваши адреса сохраняются и снова заработают, когда импорт вернётся.',
'notif.action.undo': 'Отменить',
'notif.action.keep': 'Оставить',
'notif.action.review': 'Проверить',
@@ -113,6 +113,10 @@ const sk: NotificationLocale = {
title: 'Preposlané rezervácie na kontrolu',
body: `Preposlané rezervácie čakajúce na vás v TREK: ${p.count}. Pri každej je uvedené, prečo nebola pridaná sama.`,
}),
mail_ingest_paused: () => ({
title: 'Import e-mailov pozastavený',
body: 'TREK zatiaľ neprijíma preposlané e-maily s rezerváciami, preto ich zatiaľ nepreposielajte. Vaše adresy zostávajú a znova budú fungovať, keď sa import vráti, a už pridané rezervácie zostávajú vo vašich cestách.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -61,6 +61,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Vrátiť späť',
'notif.action.keep': 'Ponechať',
'notif.action.review': 'Skontrolovať',
@@ -107,6 +107,10 @@ const en: NotificationLocale = {
title: 'Vidarebefordrade bokningar att granska',
body: `Vidarebefordrade bokningar som väntar på dig i TREK: ${p.count}. Vid varje står varför den inte lades till av sig själv.`,
}),
mail_ingest_paused: () => ({
title: 'Mejlimport pausad',
body: 'TREK tar inte emot vidarebefordrade bokningsmejl just nu, så vänta med att vidarebefordra dem. Dina adresser finns kvar och fungerar igen när importen är tillbaka, och bokningar som redan lagts till finns kvar i dina resor.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -60,6 +60,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Ångra',
'notif.action.keep': 'Behåll',
'notif.action.review': 'Granska',
@@ -100,6 +100,10 @@ const th: NotificationLocale = {
title: 'การจองที่ส่งต่อซึ่งรอตรวจสอบ',
body: `การจองที่ส่งต่อซึ่งรอคุณอยู่ใน TREK: ${p.count} แต่ละรายการบอกเหตุผลที่ไม่ได้เพิ่มเองโดยอัตโนมัติ`,
}),
mail_ingest_paused: () => ({
title: 'หยุดการนำเข้าอีเมลชั่วคราว',
body: 'ขณะนี้ TREK ยังไม่รับอีเมลการจองที่ส่งต่อมา โปรดรอก่อนส่งต่อ ที่อยู่ของคุณยังคงอยู่และจะใช้งานได้อีกครั้งเมื่อการนำเข้ากลับมา และการจองที่เพิ่มแล้วยังคงอยู่ในทริปของคุณ',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -59,6 +59,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} เพิ่ม {booking} ลงใน {trip} จากอีเมลที่ส่งต่อ',
'notif.mail_booking_pending.title': 'การจองที่ส่งต่อซึ่งรอตรวจสอบ',
'notif.mail_booking_pending.text': 'การจองที่ส่งต่อซึ่งรอคุณอยู่: {count}',
'notif.mail_ingest_paused.title': 'หยุดการนำเข้าอีเมลชั่วคราว',
'notif.mail_ingest_paused.text': 'ขณะนี้ยังไม่รับอีเมลการจองที่ส่งต่อมา ที่อยู่ของคุณยังคงอยู่และจะใช้งานได้อีกครั้งเมื่อการนำเข้ากลับมา',
'notif.action.undo': 'เลิกทำ',
'notif.action.keep': 'เก็บไว้',
'notif.action.review': 'ตรวจสอบ',
@@ -110,6 +110,10 @@ const tr: NotificationLocale = {
title: 'İncelenecek iletilen rezervasyonlar',
body: `TREK içinde seni bekleyen iletilen rezervasyonlar: ${p.count}. Her birinde neden kendiliğinden eklenmediği yazıyor.`,
}),
mail_ingest_paused: () => ({
title: 'E-posta içe aktarımı duraklatıldı',
body: 'TREK şimdilik iletilen rezervasyon e-postalarını kabul etmiyor, bu yüzden şimdilik iletme. Adreslerin kalıyor ve içe aktarım geri geldiğinde yeniden çalışacak, zaten eklenmiş rezervasyonlar da seyahatlerinde kalıyor.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -61,6 +61,8 @@ const notif: TranslationStrings = {
'{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_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.',
'notif.action.undo': 'Geri al',
'notif.action.keep': 'Tut',
'notif.action.review': 'İncele',
@@ -111,6 +111,10 @@ const uk: NotificationLocale = {
title: 'Переслані бронювання для перевірки',
body: `Пересланих бронювань чекає на вас у TREK: ${p.count}. Біля кожного вказано, чому його не додано автоматично.`,
}),
mail_ingest_paused: () => ({
title: 'Імпорт листів призупинено',
body: 'TREK поки що не приймає переслані листи з бронюваннями, тож поки не пересилайте їх. Ваші адреси зберігаються й знову працюватимуть, коли імпорт повернеться, а вже додані бронювання залишаються у ваших подорожах.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -62,6 +62,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} додав(ла) {booking} до {trip} з пересланого листа',
'notif.mail_booking_pending.title': 'Переслані бронювання для перевірки',
'notif.mail_booking_pending.text': 'Пересланих бронювань чекає на вас: {count}',
'notif.mail_ingest_paused.title': 'Імпорт листів призупинено',
'notif.mail_ingest_paused.text': 'Переслані листи з бронюваннями поки що не приймаються. Ваші адреси зберігаються й знову працюватимуть, коли імпорт повернеться.',
'notif.action.undo': 'Скасувати',
'notif.action.keep': 'Залишити',
'notif.action.review': 'Перевірити',
@@ -100,6 +100,10 @@ const en: NotificationLocale = {
title: 'Đặt chỗ chuyển tiếp cần xem lại',
body: `Đặt chỗ chuyển tiếp đang chờ bạn trong TREK: ${p.count}. Mỗi mục cho biết vì sao nó không được tự động thêm.`,
}),
mail_ingest_paused: () => ({
title: 'Đã tạm dừng nhập email',
body: 'Hiện tại TREK chưa nhận email đặt chỗ được chuyển tiếp, vì vậy hãy tạm chưa chuyển tiếp. Đị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, còn các đặt chỗ đã thêm vẫn nằm trong chuyến đi của bạn.',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -59,6 +59,8 @@ const notif: TranslationStrings = {
'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_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.',
'notif.action.undo': 'Hoàn tác',
'notif.action.keep': 'Giữ lại',
'notif.action.review': 'Xem lại',
@@ -101,6 +101,10 @@ const zhTW: NotificationLocale = {
title: '待檢查的轉寄預訂',
body: `TREK 中等待你處理的轉寄預訂:${p.count}。每一筆都說明了為什麼沒有自動新增。`,
}),
mail_ingest_paused: () => ({
title: '郵件匯入已暫停',
body: 'TREK 暫時不接收轉寄的預訂郵件,請先不要轉寄。你的地址會保留,匯入恢復後即可再次使用,已新增的預訂仍保留在你的行程中。',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -59,6 +59,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} 從轉寄的郵件將 {booking} 新增到 {trip}',
'notif.mail_booking_pending.title': '待檢查的轉寄預訂',
'notif.mail_booking_pending.text': '等待你處理的轉寄預訂:{count}',
'notif.mail_ingest_paused.title': '郵件匯入已暫停',
'notif.mail_ingest_paused.text': '暫時不接收轉寄的預訂郵件。你的地址會保留,匯入恢復後即可再次使用。',
'notif.action.undo': '復原',
'notif.action.keep': '保留',
'notif.action.review': '檢查',
@@ -101,6 +101,10 @@ const zh: NotificationLocale = {
title: '待查看的转发预订',
body: `TREK 中等待你处理的转发预订:${p.count}。每一条都说明了为什么没有自动添加。`,
}),
mail_ingest_paused: () => ({
title: '邮件导入已暂停',
body: 'TREK 暂时不接收转发的预订邮件,请先不要转发。你的地址会保留,导入恢复后即可再次使用,已添加的预订仍保留在你的行程中。',
}),
plugin_notification: (p) => ({ title: p.title ?? '', body: p.body ?? '' }),
},
passwordReset: {
+2
View File
@@ -59,6 +59,8 @@ const notif: TranslationStrings = {
'notif.mail_booking_added.shared': '{actor} 从转发的邮件中将 {booking} 添加到 {trip}',
'notif.mail_booking_pending.title': '待查看的转发预订',
'notif.mail_booking_pending.text': '等待你处理的转发预订:{count}',
'notif.mail_ingest_paused.title': '邮件导入已暂停',
'notif.mail_ingest_paused.text': '暂时不接收转发的预订邮件。你的地址会保留,导入恢复后即可再次使用。',
'notif.action.undo': '撤销',
'notif.action.keep': '保留',
'notif.action.review': '查看',