diff --git a/Cargo.lock b/Cargo.lock index 1f8ab87..bf2b38f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,7 @@ dependencies = [ "bytes 1.11.1", "chrono", "clap", + "cron", "dashmap", "deunicode", "email_address", @@ -875,6 +876,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "cron" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +dependencies = [ + "chrono", + "once_cell", + "winnow 0.6.26", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -5611,6 +5623,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.15" diff --git a/crates/admin/src/meta.rs b/crates/admin/src/meta.rs index 9992a07..ac9eb12 100644 --- a/crates/admin/src/meta.rs +++ b/crates/admin/src/meta.rs @@ -261,6 +261,7 @@ impl From for AccountModel { imap_quota_window: None, imap_quota_bytes: None, auto_download_new_mailboxes: None, + download_schedule: None, } } } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 4ae44ba..83bfc9c 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -69,5 +69,6 @@ tokio-util.workspace = true whichlang = "0.1.1" deunicode = "1.6.2" scopeguard = "1.2.0" +cron = "0.15" quick-xml = { version = "0.40.0", features = ["serialize"] } hickory-resolver = "0.26.0-alpha.1" diff --git a/crates/core/src/account/migration.rs b/crates/core/src/account/migration.rs index 23729a5..e0250c7 100644 --- a/crates/core/src/account/migration.rs +++ b/crates/core/src/account/migration.rs @@ -95,6 +95,7 @@ pub struct Account { pub imap_quota_bytes: Option, pub imap_quota_window: Option, pub auto_download_new_mailboxes: Option, + pub download_schedule: Option, } impl MemDbModel for Account { @@ -133,6 +134,7 @@ impl Account { auto_download_new_mailboxes: request.auto_download_new_mailboxes, imap_quota_bytes: request.imap_quota_bytes, imap_quota_window: request.imap_quota_window, + download_schedule: request.download_schedule, }) } @@ -439,6 +441,12 @@ impl Account { if let Some(auto_download_new_mailboxes) = request.auto_download_new_mailboxes { new.auto_download_new_mailboxes = Some(auto_download_new_mailboxes); } + if let Some(download_schedule) = request.download_schedule { + new.download_schedule = Some(download_schedule); + } + if request.clear_download_schedule == Some(true) { + new.download_schedule = None; + } new.updated_at = utc_now!(); Ok(new) } diff --git a/crates/core/src/account/payload.rs b/crates/core/src/account/payload.rs index 93eec5e..34256d1 100644 --- a/crates/core/src/account/payload.rs +++ b/crates/core/src/account/payload.rs @@ -16,6 +16,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use std::str::FromStr; + use crate::account::entity::ImapConfig; use crate::account::migration::{AccountModel, AccountType, QuotaWindow}; use crate::account::since::{DateSince, RelativeDate}; @@ -54,6 +56,7 @@ pub struct AccountCreateRequest { pub imap_quota_bytes: Option, pub imap_quota_window: Option, pub auto_download_new_mailboxes: Option, + pub download_schedule: Option, } impl AccountCreateRequest { @@ -92,12 +95,15 @@ impl AccountCreateRequest { )) } } - if self.download_interval_min.is_none() { + if self.download_interval_min.is_none() && self.download_schedule.is_none() { return Err(raise_error!( - "`sync_interval_min` is required for IMAP account type".into(), + "`sync_interval_min` or `download_schedule` is required for IMAP account type".into(), ErrorCode::InvalidParameter )); } + if let Some(ref schedule) = self.download_schedule { + validate_cron_expression(schedule)?; + } } AccountType::NoSync => {} } @@ -178,6 +184,8 @@ pub struct AccountUpdateRequest { pub imap_quota_bytes: Option, pub imap_quota_window: Option, pub auto_download_new_mailboxes: Option, + pub download_schedule: Option, + pub clear_download_schedule: Option, } impl AccountUpdateRequest { @@ -230,11 +238,36 @@ impl AccountUpdateRequest { )); } } + if self.clear_download_schedule == Some(true) && self.download_schedule.is_some() { + return Err(raise_error!( + "clear_download_schedule cannot be combined with download_schedule".into(), + ErrorCode::InvalidParameter + )); + } + if let Some(ref schedule) = self.download_schedule { + validate_cron_expression(schedule)?; + } } Ok(()) } } +fn validate_cron_expression(expr: &str) -> BichonResult<()> { + if expr.trim().is_empty() { + return Err(raise_error!( + "download_schedule must not be empty".into(), + ErrorCode::InvalidParameter + )); + } + cron::Schedule::from_str(expr).map_err(|e| { + raise_error!( + format!("Invalid cron expression '{}': {}", expr, e), + ErrorCode::InvalidParameter + ) + })?; + Ok(()) +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[cfg_attr(feature = "web-api", derive(poem_openapi::Object))] @@ -253,3 +286,34 @@ pub fn filter_accessible_accounts<'a>( .cloned() .collect() } + +#[cfg(test)] +mod test { + use super::validate_cron_expression; + + #[test] + fn valid_cron_expressions() { + assert!(validate_cron_expression("0 0 0 * * *").is_ok()); // daily at midnight + assert!(validate_cron_expression("0 */5 * * * *").is_ok()); // every 5 minutes + assert!(validate_cron_expression("0 0 12 * * 1-5").is_ok()); // weekdays at noon + assert!(validate_cron_expression("0 30 4 1 * *").is_ok()); // 1st of month at 04:30 + assert!(validate_cron_expression("0 0 * * * *").is_ok()); // every hour + } + + #[test] + fn invalid_cron_expression_too_few_fields() { + assert!(validate_cron_expression("0 0 * *").is_err()); + assert!(validate_cron_expression("* * * * *").is_err()); // 5 fields, needs seconds + } + + #[test] + fn invalid_cron_expression_empty() { + assert!(validate_cron_expression("").is_err()); + assert!(validate_cron_expression(" ").is_err()); + } + + #[test] + fn invalid_cron_expression_garbage() { + assert!(validate_cron_expression("not a cron").is_err()); + } +} diff --git a/crates/core/src/account/view.rs b/crates/core/src/account/view.rs index b594766..6f9e34f 100644 --- a/crates/core/src/account/view.rs +++ b/crates/core/src/account/view.rs @@ -57,6 +57,7 @@ pub struct AccountResp { pub imap_quota_bytes: Option, pub imap_quota_window: Option, pub auto_download_new_mailboxes: Option, + pub download_schedule: Option, } impl AccountResp { @@ -93,6 +94,7 @@ impl AccountResp { imap_quota_bytes: account.imap_quota_bytes, imap_quota_window: account.imap_quota_window, auto_download_new_mailboxes: account.auto_download_new_mailboxes, + download_schedule: account.download_schedule, } } } diff --git a/crates/core/src/cache/imap/download/download_type.rs b/crates/core/src/cache/imap/download/download_type.rs index 5327a27..2c27d73 100644 --- a/crates/core/src/cache/imap/download/download_type.rs +++ b/crates/core/src/cache/imap/download/download_type.rs @@ -16,6 +16,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use std::str::FromStr; + +use chrono::{DateTime, Local, TimeZone, Utc}; +use cron::Schedule; + use crate::{ utc_now, { @@ -48,11 +53,20 @@ pub async fn decide_next_download_task( let should_start = match trigger_type { TriggerType::Manual => true, - TriggerType::Scheduled => should_trigger_next_download( - state.last_trigger_at, - state.last_finished_at.unwrap_or(0), - account.download_interval_min.unwrap_or(60), - ), + TriggerType::Scheduled => { + let now = utc_now!(); + let cooldown_ok = now - state.last_finished_at.unwrap_or(0) > 60 * 1000; + if !cooldown_ok { + false + } else if let Some(ref schedule) = account.download_schedule { + should_trigger_scheduled(schedule, state.last_trigger_at) + } else { + should_trigger_next_download( + state.last_trigger_at, + account.download_interval_min.unwrap_or(60), + ) + } + } }; if should_start { @@ -65,9 +79,87 @@ pub async fn decide_next_download_task( fn should_trigger_next_download( last_trigger_at: i64, - last_finished_at: i64, sync_interval_min: i64, ) -> bool { let now = utc_now!(); - now - last_trigger_at > (sync_interval_min * 60 * 1000) && now - last_finished_at > 60 * 1000 + now - last_trigger_at > (sync_interval_min * 60 * 1000) +} + +fn should_trigger_scheduled(schedule_str: &str, last_trigger_at: i64) -> bool { + let schedule = match Schedule::from_str(schedule_str) { + Ok(s) => s, + Err(e) => { + tracing::warn!( + "Invalid cron expression '{}', falling back to no trigger: {}", + schedule_str, + e + ); + return false; + } + }; + // last_trigger_at is a UTC millis timestamp; convert to server local time + let last_utc = match Utc.timestamp_millis_opt(last_trigger_at) { + chrono::LocalResult::Single(dt) => dt, + _ => { + tracing::warn!("Invalid last_trigger_at timestamp: {}", last_trigger_at); + return false; + } + }; + let last_dt: DateTime = last_utc.with_timezone(&Local); + let now = Local::now(); + schedule.after(&last_dt).next().map_or(false, |next| next <= now) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn cron_every_minute_triggers_after_60s() { + // "0 * * * * *" = every minute at second 0. last_trigger 90s ago → should trigger + let now = Local::now(); + let last_trigger = now.timestamp_millis() - 90_000; + assert!(should_trigger_scheduled("0 * * * * *", last_trigger)); + } + + #[test] + fn cron_daily_midnight_triggers_when_missed() { + // "0 0 0 * * *" = daily at midnight + // last_trigger was 25 hours ago → should trigger (we missed midnight) + let now = Local::now(); + let last_trigger = now.timestamp_millis() - 25 * 60 * 60 * 1000; + assert!(should_trigger_scheduled("0 0 0 * * *", last_trigger)); + } + + #[test] + fn cron_daily_midnight_no_trigger_if_already_fired() { + // "0 0 0 * * *" = daily at midnight + // last_trigger was 1 minute ago → should NOT trigger + let now = Local::now(); + let last_trigger = now.timestamp_millis() - 60_000; + assert!(!should_trigger_scheduled("0 0 0 * * *", last_trigger)); + } + + #[test] + fn invalid_cron_returns_false() { + assert!(!should_trigger_scheduled("invalid cron expression", 0)); + } + + #[test] + fn cron_every_hour_triggers() { + // "0 0 * * * *" = every hour at minute 0, second 0 + // last_trigger was 61 minutes ago → should trigger + let now = Local::now(); + let last_trigger = now.timestamp_millis() - 61 * 60 * 1000; + assert!(should_trigger_scheduled("0 0 * * * *", last_trigger)); + } + + #[test] + fn cron_every_hour_no_trigger_if_recent() { + // "0 0 * * * *" = every hour at minute 0, second 0 + // last_trigger was just 1 minute ago → should NOT trigger (except at :00/:01 boundary) + let now = Local::now(); + let last_trigger = now.timestamp_millis() - 60_000; + assert!(!should_trigger_scheduled("0 0 * * * *", last_trigger)); + } } diff --git a/web/src/api/account/api.ts b/web/src/api/account/api.ts index 3130162..e7bb249 100644 --- a/web/src/api/account/api.ts +++ b/web/src/api/account/api.ts @@ -143,6 +143,7 @@ export interface AccountModel { imap_quota_window?: QuotaWindow; imap_quota_bytes?: number; auto_download_new_mailboxes?: boolean; + download_schedule?: string; } export const download_state = async (account_id: number) => { diff --git a/web/src/features/accounts/components/__tests__/schema.test.ts b/web/src/features/accounts/components/__tests__/schema.test.ts index 50a7293..056595f 100644 --- a/web/src/features/accounts/components/__tests__/schema.test.ts +++ b/web/src/features/accounts/components/__tests__/schema.test.ts @@ -213,37 +213,6 @@ describe('Account Form Schema', () => { }) }) - describe('folder_limit field', () => { - it('accepts undefined folder_limit', () => { - const result = getAccountSchema(false, t).safeParse(validAccountData) - expect(result.success).toBe(true) - }) - - it('accepts null folder_limit', () => { - const result = getAccountSchema(false, t).safeParse({ - ...validAccountData, - folder_limit: null, - }) - expect(result.success).toBe(true) - }) - - it('rejects folder_limit less than 100', () => { - const result = getAccountSchema(false, t).safeParse({ - ...validAccountData, - folder_limit: 50, - }) - expect(result.success).toBe(false) - }) - - it('accepts folder_limit of exactly 100', () => { - const result = getAccountSchema(false, t).safeParse({ - ...validAccountData, - folder_limit: 100, - }) - expect(result.success).toBe(true) - }) - }) - describe('account_name and login_name fields', () => { it('accepts undefined account_name and login_name', () => { const result = getAccountSchema(false, t).safeParse(validAccountData) @@ -266,6 +235,45 @@ describe('Account Form Schema', () => { expect(result.success).toBe(true) }) }) + + describe('download_schedule field', () => { + it('accepts undefined download_schedule', () => { + const result = getAccountSchema(false, t).safeParse(validAccountData) + expect(result.success).toBe(true) + }) + + it('accepts valid 6-field cron expression', () => { + const result = getAccountSchema(false, t).safeParse({ + ...validAccountData, + download_schedule: '0 0 0 * * *', + }) + expect(result.success).toBe(true) + }) + + it('accepts cron with */step syntax', () => { + const result = getAccountSchema(false, t).safeParse({ + ...validAccountData, + download_schedule: '0 */30 8-17 * * 1-5', + }) + expect(result.success).toBe(true) + }) + + it('rejects cron with too few fields', () => { + const result = getAccountSchema(false, t).safeParse({ + ...validAccountData, + download_schedule: '0 0 *', + }) + expect(result.success).toBe(false) + }) + + it('accepts empty string cron (treated as not set)', () => { + const result = getAccountSchema(false, t).safeParse({ + ...validAccountData, + download_schedule: '', + }) + expect(result.success).toBe(true) + }) + }) }) describe('Auth Config Schema (password validation)', () => { diff --git a/web/src/features/accounts/components/account-detail.tsx b/web/src/features/accounts/components/account-detail.tsx index 11e54a5..918ab11 100644 --- a/web/src/features/accounts/components/account-detail.tsx +++ b/web/src/features/accounts/components/account-detail.tsx @@ -133,8 +133,8 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { )}
- {t('accounts.folderLimit')}: - {currentRow.folder_limit ? currentRow.folder_limit : t('accounts.notAvailable')} + {t('accounts.downloadSchedule')}: + {currentRow.download_schedule || t('accounts.notAvailable')}
diff --git a/web/src/features/accounts/components/action-dialog.tsx b/web/src/features/accounts/components/action-dialog.tsx index cdb6296..ac7ef2f 100644 --- a/web/src/features/accounts/components/action-dialog.tsx +++ b/web/src/features/accounts/components/action-dialog.tsx @@ -49,7 +49,7 @@ export type Steps = [...Step[]]; const getSteps = (t: (key: string) => string): Steps => [ { id: "step-1", name: t('accounts.steps.emailAddress'), fields: ["email", "account_name"] }, { id: "step-2", name: t('accounts.steps.imap'), fields: ["imap", "use_dangerous", "login_name"] }, - { id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "date_before", "folder_limit", "download_interval_min", "download_batch_size", "auto_download_new_mailboxes"] }, + { id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "date_before", "download_interval_min", "download_batch_size", "auto_download_new_mailboxes", "download_schedule"] }, { id: "step-4", name: t('accounts.steps.summary'), fields: [] }, ]; @@ -79,10 +79,10 @@ const defaultValues: Account = { use_dangerous: false, date_since: undefined, date_before: undefined, - folder_limit: undefined, download_interval_min: 60, download_batch_size: 30, auto_download_new_mailboxes: true, + download_schedule: undefined, }; const emptyImap: ImapConfig = { @@ -109,10 +109,10 @@ const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => { use_dangerous: currentRow.use_dangerous, date_since: currentRow.date_since ?? undefined, date_before: currentRow.date_before ?? undefined, - folder_limit: currentRow.folder_limit ?? undefined, download_interval_min: currentRow.download_interval_min ?? 60, download_batch_size: currentRow.download_batch_size ?? 30, auto_download_new_mailboxes: currentRow.auto_download_new_mailboxes ?? true, + download_schedule: currentRow.download_schedule ?? undefined, }; }; @@ -191,18 +191,18 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) { use_dangerous: data.use_dangerous, date_since: data.date_since, date_before: data.date_before, - folder_limit: data.folder_limit, download_interval_min: data.download_interval_min, download_batch_size: data.download_batch_size, auto_download_new_mailboxes: data.auto_download_new_mailboxes, + download_schedule: data.download_schedule || null, }; if (isEdit) { const isAllMode = !data.date_since && !data.date_before; - const clear_folder_limit = !data.folder_limit; + const clear_download_schedule = !data.download_schedule && currentRow?.download_schedule; updateMutation.mutate({ ...commonData, ...(isAllMode ? { clear_date_range: true } : {}), - ...(clear_folder_limit ? { clear_folder_limit: true } : {}) + ...(clear_download_schedule ? { clear_download_schedule: true } : {}) }); } else { createMutation.mutate({ ...commonData, account_type: "IMAP" }); diff --git a/web/src/features/accounts/components/columns.tsx b/web/src/features/accounts/components/columns.tsx index d92041a..0e8be32 100644 --- a/web/src/features/accounts/components/columns.tsx +++ b/web/src/features/accounts/components/columns.tsx @@ -106,9 +106,12 @@ export function useColumns(): ColumnDef[] { if (account_type === "NoSync") { return n/a } + if (row.original.download_schedule) { + return {row.original.download_schedule} + } return {row.original.download_interval_min} min }, - meta: { className: 'text-center max-w-[120px]' }, + meta: { className: 'text-center max-w-[160px]' }, enableHiding: false, }, { diff --git a/web/src/features/accounts/components/data-table-row-actions.tsx b/web/src/features/accounts/components/data-table-row-actions.tsx index 8607517..5187882 100644 --- a/web/src/features/accounts/components/data-table-row-actions.tsx +++ b/web/src/features/accounts/components/data-table-row-actions.tsx @@ -98,26 +98,6 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { - - {showDownload && ( - - {t('accounts.startDownload')} - - - - - )} - - - {showDownload && ( - - {t('accounts.cancelDownload')} - - - - - )} - {showDownload && } {hasPermission && { setCurrentRow(row.original) @@ -169,6 +149,27 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { } {hasPermission && } + + {showDownload && ( + + {t('accounts.startDownload')} + + + + + )} + + + {showDownload && ( + + {t('accounts.cancelDownload')} + + + + + )} + {showDownload && } + {hasPermission && { setCurrentRow(row.original) diff --git a/web/src/features/accounts/components/schema.ts b/web/src/features/accounts/components/schema.ts index 700375a..50d6cdf 100644 --- a/web/src/features/accounts/components/schema.ts +++ b/web/src/features/accounts/components/schema.ts @@ -79,12 +79,6 @@ export const getAccountSchema = (isEdit: boolean, t: (key: string) => string) => use_dangerous: z.boolean(), date_since: dateSelectionSchema(t).optional(), date_before: relativeDateSchema(t).optional(), - folder_limit: z - .number({ invalid_type_error: t('validation.folderLimitMustBeNumber') }) - .int() - .min(100, { message: t('validation.folderLimitMustBeAtLeast100') }) - .nullable() - .optional(), download_interval_min: z .number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber'), @@ -107,6 +101,18 @@ export const getAccountSchema = (isEdit: boolean, t: (key: string) => string) => message: t('validation.singleRequestBatchSizeTooLarge'), }), auto_download_new_mailboxes: z.boolean(), + download_schedule: z + .string() + .optional() + .refine( + (val) => { + if (!val || val.trim() === '') return true; + const fields = val.trim().split(/\s+/); + if (fields.length < 6) return false; + return true; + }, + { message: t('validation.invalidCronExpression') } + ), }) export type AccountFormValues = z.infer< diff --git a/web/src/features/accounts/components/step3.tsx b/web/src/features/accounts/components/step3.tsx index 1a947b2..bd44a46 100644 --- a/web/src/features/accounts/components/step3.tsx +++ b/web/src/features/accounts/components/step3.tsx @@ -48,6 +48,72 @@ import i18n from "@/i18n"; type SyncMode = 'all' | 'since_fixed' | 'since_relative' | 'before_relative'; +type ScheduleMode = 'interval' | 'cron'; +type CronMode = 'simple' | 'advanced'; +type CronFrequency = 'daily' | 'weekly' | 'monthly'; + +interface CronSimpleState { + frequency: CronFrequency; + hour: number; + minute: number; + dayOfWeek: number; + dayOfMonth: number; +} + +const DEFAULT_CRON_SIMPLE: CronSimpleState = { + frequency: 'daily', + hour: 0, + minute: 0, + dayOfWeek: 1, + dayOfMonth: 1, +}; + +function buildCronFromSimple(s: CronSimpleState): string { + switch (s.frequency) { + case 'daily': + return `0 ${s.minute} ${s.hour} * * *`; + case 'weekly': + return `0 ${s.minute} ${s.hour} * * ${s.dayOfWeek}`; + case 'monthly': + return `0 ${s.minute} ${s.hour} ${s.dayOfMonth} * *`; + } +} + +function tryParseCronToSimple(cron: string): CronSimpleState | null { + const fields = cron.trim().split(/\s+/); + if (fields.length < 6) return null; + + const sec = fields[0]; + const min = fields[1]; + const hour = fields[2]; + const dom = fields[3]; + const month = fields[4]; + const dow = fields[5]; + + if (sec !== '0') return null; + if (month !== '*') return null; + + const minuteVal = parseInt(min, 10); + const hourVal = parseInt(hour, 10); + if (isNaN(minuteVal) || isNaN(hourVal)) return null; + + if (dom === '*' && dow === '*') { + return { frequency: 'daily', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: 1 }; + } + if (dom === '*') { + const dowVal = parseInt(dow, 10); + if (!isNaN(dowVal)) { + return { frequency: 'weekly', hour: hourVal, minute: minuteVal, dayOfWeek: dowVal, dayOfMonth: 1 }; + } + } + if (dow === '*') { + const domVal = parseInt(dom, 10); + if (!isNaN(domVal)) { + return { frequency: 'monthly', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: domVal }; + } + } + return null; +} export default function Step3() { const { t } = useTranslation(); @@ -61,6 +127,31 @@ export default function Step3() { return 'all'; }); + const [scheduleMode, setScheduleMode] = useState(() => { + if (current.download_schedule) return 'cron'; + return 'interval'; + }); + + const [cronMode, setCronMode] = useState(() => { + if (current.download_schedule && tryParseCronToSimple(current.download_schedule)) { + return 'simple'; + } + if (current.download_schedule) return 'advanced'; + return 'simple'; + }); + + const [cronSimple, setCronSimple] = useState(() => { + if (current.download_schedule) { + return tryParseCronToSimple(current.download_schedule) ?? DEFAULT_CRON_SIMPLE; + } + return DEFAULT_CRON_SIMPLE; + }); + + const updateCronFromSimple = (partial: Partial) => { + const next = { ...cronSimple, ...partial }; + setCronSimple(next); + setValue('download_schedule', buildCronFromSimple(next)); + }; const handleModeChange = (mode: SyncMode) => { setSyncMode(mode); @@ -77,41 +168,231 @@ export default function Step3() { } }; + const handleScheduleModeChange = (mode: ScheduleMode) => { + setScheduleMode(mode); + if (mode === 'interval') { + setValue("download_schedule", undefined); + } else { + setValue("download_interval_min", 60); + if (cronMode === 'simple') { + updateCronFromSimple(cronSimple); + } + } + }; + return (
-
- ( - - {t('accounts.downloadInterval')} - - field.onChange(parseInt(e.target.value, 10))} /> - - - - {t('accounts.downloadIntervalPlaceholder')} - - +
+ + {t('accounts.scheduleMode')} + + {t('accounts.scheduleModeDescription')} + + + +
+ {scheduleMode === 'interval' ? ( + ( + + {t('accounts.downloadInterval')} + + field.onChange(parseInt(e.target.value, 10))} /> + + + + {t('accounts.downloadIntervalPlaceholder')} + + + )} + /> + ) : ( +
+
+ {/* {t('accounts.downloadSchedule')} */} +
+ + +
+
+ + {cronMode === 'simple' ? ( +
+ + {t('accounts.cronFrequency')} + + + + + {t('accounts.cronHour')} + + + + : + + + {t('accounts.cronMinute')} + + + + {cronSimple.frequency === 'weekly' && ( + + {t('accounts.cronDayOfWeek')} + + + )} + + {cronSimple.frequency === 'monthly' && ( + + {t('accounts.cronDayOfMonth')} + + + )} + +
+ = {buildCronFromSimple(cronSimple)} +
+
+ ) : ( + ( + + + + + + {t('accounts.downloadScheduleDescription')} + + + + )} + /> + )} + + {cronMode === 'simple' && ( + {t('accounts.downloadScheduleDescription')} + )} + +
+ {t('accounts.cronTimezoneNote')} +
+
)} - /> - ( - - {t('accounts.downloadBatchSize')} - - field.onChange(parseInt(e.target.value, 10))} /> - - - - {t('accounts.downloadBatchSizeDescription')} - - - )} - /> + ( + + {t('accounts.downloadBatchSize')} + + field.onChange(parseInt(e.target.value, 10))} /> + + + + {t('accounts.downloadBatchSizeDescription')} + + + )} + /> +

- - ( - - {t('accounts.folderLimit')} - {t('accounts.folderLimitDescription')} - - { - const value = e.target.value; - field.onChange(value === '' ? null : Number(value)); - }} - /> - - - - )} - />
); } \ No newline at end of file diff --git a/web/src/features/accounts/components/step4.tsx b/web/src/features/accounts/components/step4.tsx index 29a4226..07276a4 100644 --- a/web/src/features/accounts/components/step4.tsx +++ b/web/src/features/accounts/components/step4.tsx @@ -50,7 +50,7 @@ export default function Step4() { return (
- + {t('accounts.email')}: {summaryData.email} @@ -154,11 +154,6 @@ export default function Step4() { - - {t('accounts.folderLimit')}: - {summaryData.folder_limit ?? t('accounts.notAvailable')} - - {t('accounts.downloadInterval')}: {summaryData.download_interval_min} {t('accounts.minutes')} @@ -169,6 +164,11 @@ export default function Step4() { {summaryData.download_batch_size} + + {t('accounts.downloadSchedule')}: + {summaryData.download_schedule || t('accounts.notAvailable')} + + {t('accounts.autoDownloadNewMailboxes')}: {summaryData.auto_download_new_mailboxes ? t('common.yes') : t('common.no')} diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index c289a37..48a7cc9 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -115,6 +115,24 @@ "continue": "متابعة", "createdAt": "تاريخ الإنشاء", "creationFailed": "فشل الإنشاء، يرجى المحاولة مرة أخرى لاحقًا", + "cronAdvanced": "تعبير متقدم", + "cronDaily": "يومياً", + "cronDayOfMonth": "اليوم من الأسبوع", + "cronDayOfWeek": "اليوم من الأسبوع", + "cronFrequency": "التكرار", + "cronFriday": "الجمعة", + "cronHour": "الساعة", + "cronMinute": "الدقيقة", + "cronMonday": "الإثنين", + "cronMonthly": "شهرياً", + "cronSaturday": "السبت", + "cronSimple": "تعبير بسيط", + "cronSunday": "الأحد", + "cronThursday": "الخميس", + "cronTimezoneNote": "جميع الأوقات بالتوقيت المحلي للخادم.", + "cronTuesday": "الثلاثاء", + "cronWednesday": "الأربعاء", + "cronWeekly": "أسبوعياً", "dateSelection": "تحديد التاريخ", "dateSince": "التاريخ منذ", "days": "أيام", @@ -133,6 +151,9 @@ "downloadFailed": "فشل بدء مهمة التنزيل", "downloadInterval": "دورة التنزيل (بالدقائق)", "downloadIntervalPlaceholder": "أدخل الدقائق", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "كرون من 6 حقول (ثانية-دقائق-ساعات-يوم-شهر-أسبوع) بتوقيت الخادم. يتجاوز الفاصل الزمني.", + "downloadSchedulePlaceholder": "مثال: 0 0 * * *", "downloadScope": "استراتيجية التنزيل", "downloadScopeDescription": "اختر رسائل البريد الإلكتروني التي يجب فهرستها وتنزيلها.", "downloadStarted": "بدأت مهمة التنزيل", @@ -247,6 +268,10 @@ } }, "saveChanges": "حفظ التغييرات", + "scheduleMode": "جدولة التنزيل", + "scheduleModeCron": "تعبير Cron", + "scheduleModeDescription": "تعيين التنزيل بفواصل زمنية ثابتة أو عبر Cron.", + "scheduleModeInterval": "فواصل زمنية ثابتة", "selectAccountType": "اختر نوع الحساب", "selectAtLeastOneFolder": "يرجى تحديد مجلد واحد على الأقل", "selectAuthMethod": "اختر طريقة مصادقة", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "يجب أن يكون منفذ IMAP عددًا صحيحًا موجبًا", "incrementalSyncMustBeAtLeast10": "يجب أن يكون فاصل المزامنة التزايدية 10 دقائق على الأقل", "incrementalSyncMustBeNumber": "يجب أن يكون فاصل المزامنة التزايدية رقمًا", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "عنوان بريد إلكتروني غير صالح", "invalidUrl": "عنوان URL غير صالح", "passwordMinLength": "يجب أن تتكون كلمة المرور من {{min}} أحرف على الأقل", diff --git a/web/src/locales/da.json b/web/src/locales/da.json index ca1dc4c..b68990a 100644 --- a/web/src/locales/da.json +++ b/web/src/locales/da.json @@ -115,6 +115,24 @@ "continue": "Fortsæt", "createdAt": "Oprettet", "creationFailed": "Oprettelse mislykkedes, prøv venligst igen senere", + "cronAdvanced": "Avanceret udtryk", + "cronDaily": "Dagligt", + "cronDayOfMonth": "Ugedag", + "cronDayOfWeek": "Ugedag", + "cronFrequency": "Frekvens", + "cronFriday": "Fredag", + "cronHour": "Time", + "cronMinute": "Minut", + "cronMonday": "Mandag", + "cronMonthly": "Månedligt", + "cronSaturday": "Lørdag", + "cronSimple": "Simpelt udtryk", + "cronSunday": "Søndag", + "cronThursday": "Torsdag", + "cronTimezoneNote": "Alle tider er serverens lokale tid.", + "cronTuesday": "Tirsdag", + "cronWednesday": "Onsdag", + "cronWeekly": "Ugentligt", "dateSelection": "Datovalg", "dateSince": "Dato fra", "days": "Dage", @@ -133,6 +151,9 @@ "downloadFailed": "Kunne ikke starte download-opgave", "downloadInterval": "Downloadinterval (minutter)", "downloadIntervalPlaceholder": "Indtast minutter", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-felters Cron (sek min time dag mdr ugedag) i servertid. Tilsidesætter interval.", + "downloadSchedulePlaceholder": "F.eks.: 0 0 * * *", "downloadScope": "Downloadstrategi", "downloadScopeDescription": "Vælg hvilke e-mails der skal indekseres og downloades.", "downloadStarted": "Download-opgave startet", @@ -247,6 +268,10 @@ } }, "saveChanges": "Gem ændringer", + "scheduleMode": "Planlægning af download", + "scheduleModeCron": "Cron-udtryk", + "scheduleModeDescription": "Download via faste intervaller eller Cron.", + "scheduleModeInterval": "Fast interval", "selectAccountType": "Vælg kontotype", "selectAtLeastOneFolder": "Vælg venligst mindst én mappe", "selectAuthMethod": "Vælg en godkendelsesmetode", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP-port skal være et positivt heltal", "incrementalSyncMustBeAtLeast10": "Inkrementelt synkroniseringsinterval skal være mindst 10 minutter", "incrementalSyncMustBeNumber": "Inkrementelt synkroniseringsinterval skal være et tal", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Ugyldig e-mailadresse", "invalidUrl": "Ugyldig URL", "passwordMinLength": "Adgangskoden skal være mindst {{min}} tegn lang", diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 2f87b6c..44e1396 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -115,6 +115,24 @@ "continue": "Weiter", "createdAt": "Erstellt am", "creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es später erneut", + "cronAdvanced": "Erweiterter Ausdruck", + "cronDaily": "Täglich", + "cronDayOfMonth": "Wochentag", + "cronDayOfWeek": "Wochentag", + "cronFrequency": "Häufigkeit", + "cronFriday": "Freitag", + "cronHour": "Stunde", + "cronMinute": "Minute", + "cronMonday": "Montag", + "cronMonthly": "Monatlich", + "cronSaturday": "Samstag", + "cronSimple": "Einfacher Ausdruck", + "cronSunday": "Sonntag", + "cronThursday": "Donnerstag", + "cronTimezoneNote": "Alle Zeiten nutzen Server-Ortszeit.", + "cronTuesday": "Dienstag", + "cronWednesday": "Mittwoch", + "cronWeekly": "Wöchentlich", "dateSelection": "Datumsauswahl", "dateSince": "Datum seit", "days": "Tage", @@ -133,6 +151,9 @@ "downloadFailed": "Download-Aufgabe konnte nicht gestartet werden", "downloadInterval": "Download-Intervall (Minuten)", "downloadIntervalPlaceholder": "Minuten eingeben", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-Felder-Cron (Sek Min Std Tag Mon Wochentag) in Serverzeit. Ersetzt Intervall.", + "downloadSchedulePlaceholder": "Z. B.: 0 0 * * *", "downloadScope": "Download-Strategie", "downloadScopeDescription": "Wählen Sie aus, welche E-Mails indiziert und heruntergeladen werden sollen.", "downloadStarted": "Download-Aufgabe gestartet", @@ -247,6 +268,10 @@ } }, "saveChanges": "Änderungen speichern", + "scheduleMode": "Download-Zeitplan", + "scheduleModeCron": "Cron-Ausdruck", + "scheduleModeDescription": "Download nach festem Intervall oder Cron-Ausdruck.", + "scheduleModeInterval": "Festes Intervall", "selectAccountType": "Kontotyp auswählen", "selectAtLeastOneFolder": "Wählen Sie mindestens einen Ordner aus", "selectAuthMethod": "Authentifizierungsmethode auswählen", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "Der IMAP-Port muss eine positive ganze Zahl sein", "incrementalSyncMustBeAtLeast10": "Das inkrementelle Synchronisierungsintervall muss mindestens 10 Minuten betragen", "incrementalSyncMustBeNumber": "Das inkrementelle Synchronisierungsintervall muss eine Zahl sein", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Ungültige E-Mail-Adresse", "invalidUrl": "Ungültige URL", "passwordMinLength": "Das Passwort muss mindestens {{min}} Zeichen lang sein", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 844e820..dce0cde 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -115,6 +115,24 @@ "continue": "Continue", "createdAt": "Created At", "creationFailed": "Creation failed, please try again later", + "cronAdvanced": "Advanced Expression", + "cronDaily": "Daily", + "cronDayOfMonth": "Day of Week", + "cronDayOfWeek": "Day of Week", + "cronFrequency": "Frequency", + "cronFriday": "Friday", + "cronHour": "Hour", + "cronMinute": "Minute", + "cronMonday": "Monday", + "cronMonthly": "Monthly", + "cronSaturday": "Saturday", + "cronSimple": "Simple Expression", + "cronSunday": "Sunday", + "cronThursday": "Thursday", + "cronTimezoneNote": "All times use server local timezone.", + "cronTuesday": "Tuesday", + "cronWednesday": "Wednesday", + "cronWeekly": "Weekly", "dateSelection": "Date Selection", "dateSince": "Date Since", "days": "Days", @@ -133,6 +151,9 @@ "downloadFailed": "Failed to start download task", "downloadInterval": "Download Interval (minutes)", "downloadIntervalPlaceholder": "Enter minutes", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-field Cron (sec min hour day month dow) in server time. Overrides interval.", + "downloadSchedulePlaceholder": "e.g., 0 0 * * *", "downloadScope": "Download Strategy", "downloadScopeDescription": "Choose which emails should be indexed and downloaded.", "downloadStarted": "Download task started", @@ -247,6 +268,10 @@ } }, "saveChanges": "Save changes", + "scheduleMode": "Download Schedule", + "scheduleModeCron": "Cron Expression", + "scheduleModeDescription": "Download via fixed intervals or Cron expressions.", + "scheduleModeInterval": "Fixed Interval", "selectAccountType": "Select account type", "selectAtLeastOneFolder": "Please select at least one folder", "selectAuthMethod": "Select an authentication method", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP port must be a positive integer", "incrementalSyncMustBeAtLeast10": "Incremental sync interval must be at least 10 minutes", "incrementalSyncMustBeNumber": "Incremental sync interval must be a number", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Invalid email address", "invalidUrl": "Invalid URL", "passwordMinLength": "Password must be at least {{min}} characters long", diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 2934f4d..6608cdb 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -115,6 +115,24 @@ "continue": "Continuar", "createdAt": "Creado el", "creationFailed": "Error al crear, por favor, inténtalo de nuevo más tarde", + "cronAdvanced": "Expresión avanzada", + "cronDaily": "Diario", + "cronDayOfMonth": "Día de la semana", + "cronDayOfWeek": "Día de la semana", + "cronFrequency": "Frecuencia", + "cronFriday": "Viernes", + "cronHour": "Hora", + "cronMinute": "Minuto", + "cronMonday": "Lunes", + "cronMonthly": "Mensual", + "cronSaturday": "Sábado", + "cronSimple": "Expresión simple", + "cronSunday": "Domingo", + "cronThursday": "Jueves", + "cronTimezoneNote": "Horas en zona horaria del servidor.", + "cronTuesday": "Martes", + "cronWednesday": "Miércoles", + "cronWeekly": "Semanal", "dateSelection": "Selección de fecha", "dateSince": "Fecha desde", "days": "Días", @@ -133,6 +151,9 @@ "downloadFailed": "Error al iniciar la tarea de descarga", "downloadInterval": "Intervalo de descarga (minutos)", "downloadIntervalPlaceholder": "Ingresa los minutos", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "Cron de 6 campos (seg min hora día mes día-sem) en hora del servidor. Anula intervalo.", + "downloadSchedulePlaceholder": "ej., 0 0 * * *", "downloadScope": "Estrategia de descarga", "downloadScopeDescription": "Elija qué correos electrónicos deben indexarse y descargarse.", "downloadStarted": "Tarea de descarga iniciada", @@ -247,6 +268,10 @@ } }, "saveChanges": "Guardar cambios", + "scheduleMode": "Programación de descarga", + "scheduleModeCron": "Expresión Cron", + "scheduleModeDescription": "Descarga por intervalos fijos o expresiones Cron.", + "scheduleModeInterval": "Intervalo fijo", "selectAccountType": "Seleccionar tipo de cuenta", "selectAtLeastOneFolder": "Selecciona al menos una carpeta", "selectAuthMethod": "Selecciona el método de autenticación", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "El puerto IMAP debe ser un número entero positivo", "incrementalSyncMustBeAtLeast10": "El intervalo de sincronización incremental debe ser de al menos 10 minutos", "incrementalSyncMustBeNumber": "El intervalo de sincronización incremental debe ser un número", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Dirección de correo electrónico inválida", "invalidUrl": "URL inválida", "passwordMinLength": "La contraseña debe tener al menos {{min}} caracteres", diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json index a65d698..b5faf51 100644 --- a/web/src/locales/fi.json +++ b/web/src/locales/fi.json @@ -115,6 +115,24 @@ "continue": "Jatka", "createdAt": "Luotu", "creationFailed": "Luominen epäonnistui, yritä myöhemmin uudelleen", + "cronAdvanced": "Edistynyt lauseke", + "cronDaily": "Päivittäin", + "cronDayOfMonth": "Viikonpäivä", + "cronDayOfWeek": "Viikonpäivä", + "cronFrequency": "Tiheys", + "cronFriday": "Perjantai", + "cronHour": "Tunti", + "cronMinute": "Minuutti", + "cronMonday": "Maanantai", + "cronMonthly": "Kuukausittain", + "cronSaturday": "Lauantai", + "cronSimple": "Yksinkertainen lauseke", + "cronSunday": "Sunnuntai", + "cronThursday": "Torstai", + "cronTimezoneNote": "Kaikki ajat palvelimen paikallista aikaa.", + "cronTuesday": "Tiistai", + "cronWednesday": "Keskiviikko", + "cronWeekly": "Viikoittain", "dateSelection": "Päivämäärän valinta", "dateSince": "Päivämäärä alkaen", "days": "Päivää", @@ -133,6 +151,9 @@ "downloadFailed": "Lataustehtävän aloittaminen epäonnistui", "downloadInterval": "Latausväli (minuuttia)", "downloadIntervalPlaceholder": "Syötä minuutit", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-kenttäinen Cron (sek min tun pv kk vkpv) palvelimen ajassa. Korvaa aikavälin.", + "downloadSchedulePlaceholder": "esim. 0 0 * * *", "downloadScope": "Latausstrategia", "downloadScopeDescription": "Valitse, mitkä sähköpostit indeksoidaan ja ladataan.", "downloadStarted": "Lataustehtävä aloitettu", @@ -247,6 +268,10 @@ } }, "saveChanges": "Tallenna muutokset", + "scheduleMode": "Latauksen ajoitus", + "scheduleModeCron": "Cron-lauseke", + "scheduleModeDescription": "Lataa säännöllisin väliajoin tai Cron-lausekkeella.", + "scheduleModeInterval": "Säännöllinen väliaika", "selectAccountType": "Valitse tilityyppi", "selectAtLeastOneFolder": "Valitse vähintään yksi kansio", "selectAuthMethod": "Valitse todennusmenetelmä", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP-portin on oltava positiivinen kokonaisluku", "incrementalSyncMustBeAtLeast10": "Lisäävän synkronoinnin välin on oltava vähintään 10 minuuttia", "incrementalSyncMustBeNumber": "Lisäävän synkronoinnin välin on oltava numero", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Virheellinen sähköpostiosoite", "invalidUrl": "Virheellinen URL-osoite", "passwordMinLength": "Salasanan on oltava vähintään {{min}} merkkiä pitkä", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index 918860a..630b7fe 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -115,6 +115,24 @@ "continue": "Continuer", "createdAt": "Créé le", "creationFailed": "La création a échoué, veuillez réessayer plus tard", + "cronAdvanced": "Expression avancée", + "cronDaily": "Chaque jour", + "cronDayOfMonth": "Jour de la semaine", + "cronDayOfWeek": "Jour de la semaine", + "cronFrequency": "Fréquence", + "cronFriday": "Vendredi", + "cronHour": "Heure", + "cronMinute": "Minute", + "cronMonday": "Lundi", + "cronMonthly": "Chaque mois", + "cronSaturday": "Samedi", + "cronSimple": "Expression simple", + "cronSunday": "Dimanche", + "cronThursday": "Jeudi", + "cronTimezoneNote": "Heures au fuseau horaire du serveur.", + "cronTuesday": "Mardi", + "cronWednesday": "Mercredi", + "cronWeekly": "Chaque semaine", "dateSelection": "Sélection de Date", "dateSince": "Date Depuis", "days": "Jours", @@ -133,6 +151,9 @@ "downloadFailed": "Échec du lancement de la tâche de téléchargement", "downloadInterval": "Intervalle de téléchargement (minutes)", "downloadIntervalPlaceholder": "Entrer les minutes", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "Cron à 6 champs (sec min heure jour mois jour-sem) à l'heure du serveur. Remplace l'intervalle.", + "downloadSchedulePlaceholder": "ex. 0 0 * * *", "downloadScope": "Stratégie de téléchargement", "downloadScopeDescription": "Choisissez les e-mails à indexer et à télécharger.", "downloadStarted": "Tâche de téléchargement lancée", @@ -247,6 +268,10 @@ } }, "saveChanges": "Enregistrer les Modifications", + "scheduleMode": "Planification de téléchargement", + "scheduleModeCron": "Expression Cron", + "scheduleModeDescription": "Téléchargement par intervalle fixe ou expression Cron.", + "scheduleModeInterval": "Intervalle fixe", "selectAccountType": "Sélectionner le type de compte", "selectAtLeastOneFolder": "Veuillez sélectionner au moins un dossier", "selectAuthMethod": "Sélectionner une méthode d'authentification", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "Le port IMAP doit être un entier positif", "incrementalSyncMustBeAtLeast10": "L'intervalle de synchronisation incrémentielle doit être d'au moins 10 minutes", "incrementalSyncMustBeNumber": "L'intervalle de synchronisation incrémentielle doit être un nombre", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Adresse e-mail non valide", "invalidUrl": "URL non valide", "passwordMinLength": "Le mot de passe doit contenir au moins {{min}} caractères", diff --git a/web/src/locales/it.json b/web/src/locales/it.json index edf726e..d689de6 100644 --- a/web/src/locales/it.json +++ b/web/src/locales/it.json @@ -115,6 +115,24 @@ "continue": "Continua", "createdAt": "Creato Il", "creationFailed": "Creazione fallita, riprova più tardi", + "cronAdvanced": "Espressione avanzata", + "cronDaily": "Ogni giorno", + "cronDayOfMonth": "Giorno della settimana", + "cronDayOfWeek": "Giorno della settimana", + "cronFrequency": "Frequenza", + "cronFriday": "Venerdì", + "cronHour": "Ora", + "cronMinute": "Minuto", + "cronMonday": "Lunedì", + "cronMonthly": "Ogni mese", + "cronSaturday": "Sabato", + "cronSimple": "Espressione semplice", + "cronSunday": "Domenica", + "cronThursday": "Giovedì", + "cronTimezoneNote": "Orari nel fuso orario del server.", + "cronTuesday": "Martedì", + "cronWednesday": "Mercoledì", + "cronWeekly": "Ogni settimana", "dateSelection": "Selezione Data", "dateSince": "Data Da", "days": "Giorni", @@ -133,6 +151,9 @@ "downloadFailed": "Avvio attività di download non riuscito", "downloadInterval": "Intervallo di download (minuti)", "downloadIntervalPlaceholder": "Inserisci i minuti", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "Cron a 6 campi (sec min ora giorno mese giorno-sett) in ora del server. Sostituisce l'intervallo.", + "downloadSchedulePlaceholder": "es. 0 0 * * *", "downloadScope": "Strategia di download", "downloadScopeDescription": "Scegli quali email indicizzare e scaricare.", "downloadStarted": "Attività di download avviata", @@ -247,6 +268,10 @@ } }, "saveChanges": "Salva Modifiche", + "scheduleMode": "Pianificazione del download", + "scheduleModeCron": "Espressione Cron", + "scheduleModeDescription": "Download a intervalli fissi o tramite espressioni Cron.", + "scheduleModeInterval": "Intervallo fisso", "selectAccountType": "Seleziona tipo di account", "selectAtLeastOneFolder": "Seleziona almeno una cartella", "selectAuthMethod": "Seleziona un metodo di autenticazione", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "La porta IMAP deve essere un numero intero positivo", "incrementalSyncMustBeAtLeast10": "L'intervallo di sincronizzazione incrementale deve essere di almeno 10 minuti", "incrementalSyncMustBeNumber": "L'intervallo di sincronizzazione incrementale deve essere un numero", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Indirizzo email non valido", "invalidUrl": "URL non valido", "passwordMinLength": "La password deve contenere almeno {{min}} caratteri", diff --git a/web/src/locales/jp.json b/web/src/locales/jp.json index 65b9f12..c2827a3 100644 --- a/web/src/locales/jp.json +++ b/web/src/locales/jp.json @@ -115,6 +115,24 @@ "continue": "続行", "createdAt": "作成日時", "creationFailed": "作成に失敗しました。しばらくしてからもう一度お試しください。", + "cronAdvanced": "高度な式", + "cronDaily": "毎日", + "cronDayOfMonth": "曜日", + "cronDayOfWeek": "曜日", + "cronFrequency": "頻度", + "cronFriday": "金曜日", + "cronHour": "时", + "cronMinute": "分", + "cronMonday": "月曜日", + "cronMonthly": "毎月", + "cronSaturday": "土曜日", + "cronSimple": "簡易式", + "cronSunday": "日曜日", + "cronThursday": "木曜日", + "cronTimezoneNote": "時間はサーバーの現地時間です。", + "cronTuesday": "火曜日", + "cronWednesday": "水曜日", + "cronWeekly": "毎週", "dateSelection": "日付選択", "dateSince": "同期開始日", "days": "日", @@ -133,6 +151,9 @@ "downloadFailed": "ダウンロードタスクの起動に失敗しました", "downloadInterval": "ダウンロード間隔 (分)", "downloadIntervalPlaceholder": "分を入力してください", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "サーバー時間の6フィールドCron式(秒分時日月曜日)。設定値より優先。", + "downloadSchedulePlaceholder": "例:0 0 * * *", "downloadScope": "ダウンロード戦略", "downloadScopeDescription": "インデックスとダウンロードの対象となるメールを選択してください。", "downloadStarted": "ダウンロードタスクを開始しました", @@ -247,6 +268,10 @@ } }, "saveChanges": "変更を保存", + "scheduleMode": "ダウンロードスケジュール", + "scheduleModeCron": "Cron式", + "scheduleModeDescription": "固定間隔またはCron式でダウンロード。", + "scheduleModeInterval": "固定間隔", "selectAccountType": "アカウントの種類を選択", "selectAtLeastOneFolder": "少なくとも1つのフォルダーを選択してください", "selectAuthMethod": "認証方式を選択", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAPポートは正の整数である必要があります", "incrementalSyncMustBeAtLeast10": "増分同期間隔は10分以上である必要があります", "incrementalSyncMustBeNumber": "増分同期間隔は数値である必要があります", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "無効なメールアドレスです", "invalidUrl": "無効なURLです", "passwordMinLength": "パスワードは{{min}}文字以上である必要があります", diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index 50e7f5f..9525994 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -115,6 +115,24 @@ "continue": "계속", "createdAt": "생성일", "creationFailed": "생성에 실패했습니다. 나중에 다시 시도하십시오.", + "cronAdvanced": "고급 표현식", + "cronDaily": "매일", + "cronDayOfMonth": "요일", + "cronDayOfWeek": "요일", + "cronFrequency": "주기", + "cronFriday": "금요일", + "cronHour": "시", + "cronMinute": "분", + "cronMonday": "월요일", + "cronMonthly": "매월", + "cronSaturday": "토요일", + "cronSimple": "간단한 표현식", + "cronSunday": "일요일", + "cronThursday": "목요일", + "cronTimezoneNote": "모든 시간은 서버 현지 시간 기준입니다.", + "cronTuesday": "화요일", + "cronWednesday": "수요일", + "cronWeekly": "매주", "dateSelection": "날짜 선택", "dateSince": "동기화 시작일", "days": "일", @@ -133,6 +151,9 @@ "downloadFailed": "다운로드 작업 시작 실패", "downloadInterval": "다운로드 주기 (분)", "downloadIntervalPlaceholder": "분 단위 입력", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "서버 시간 기준 6필드 Cron(초 분 시 일 월 요일). 간격 설정보다 우선됨.", + "downloadSchedulePlaceholder": "예: 0 0 * * *", "downloadScope": "다운로드 전략", "downloadScopeDescription": "색인화 및 다운로드할 이메일을 선택하십시오.", "downloadStarted": "다운로드 작업 시작됨", @@ -247,6 +268,10 @@ } }, "saveChanges": "변경 사항 저장", + "scheduleMode": "다운로드 일정", + "scheduleModeCron": "Cron 표현식", + "scheduleModeDescription": "고정 간격 또는 Cron 표현식으로 다운로드.", + "scheduleModeInterval": "고정 간격", "selectAccountType": "계정 유형 선택", "selectAtLeastOneFolder": "하나 이상의 폴더를 선택하십시오", "selectAuthMethod": "인증 방법 선택", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP 포트는 양의 정수여야 합니다", "incrementalSyncMustBeAtLeast10": "증분 동기화 간격은 최소 10분 이상이어야 합니다", "incrementalSyncMustBeNumber": "증분 동기화 간격은 숫자여야 합니다", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "유효하지 않은 이메일 주소", "invalidUrl": "유효하지 않은 URL", "passwordMinLength": "비밀번호는 {{min}}자 이상이어야 합니다", diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json index 82a7603..22a5ff5 100644 --- a/web/src/locales/nl.json +++ b/web/src/locales/nl.json @@ -115,6 +115,24 @@ "continue": "Doorgaan", "createdAt": "Aangemaakt Op", "creationFailed": "Aanmaken mislukt, probeer het later opnieuw", + "cronAdvanced": "Geavanceerde expressie", + "cronDaily": "Dagelijks", + "cronDayOfMonth": "Dag van de week", + "cronDayOfWeek": "Dag van de week", + "cronFrequency": "Frequentie", + "cronFriday": "Vrijdag", + "cronHour": "Uur", + "cronMinute": "Minuut", + "cronMonday": "Maandag", + "cronMonthly": "Maandelijks", + "cronSaturday": "Zaterdag", + "cronSimple": "Simpele expressie", + "cronSunday": "Zondag", + "cronThursday": "Donderdag", + "cronTimezoneNote": "Alle tijden zijn server-lokale tijd.", + "cronTuesday": "Dinsdag", + "cronWednesday": "Woensdag", + "cronWeekly": "Wekelijks", "dateSelection": "Datumselectie", "dateSince": "Datum Sinds", "days": "Dagen", @@ -133,6 +151,9 @@ "downloadFailed": "Downloadtaak starten mislukt", "downloadInterval": "Download-interval (minuten)", "downloadIntervalPlaceholder": "Voer minuten in", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-velds Cron (sec min uur dag maand dag-week) in servertijd. Overschrijft interval.", + "downloadSchedulePlaceholder": "bijv. 0 0 * * *", "downloadScope": "Downloadstrategie", "downloadScopeDescription": "Kies welke e-mails moeten worden geïndexeerd en gedownload.", "downloadStarted": "Downloadtaak gestart", @@ -247,6 +268,10 @@ } }, "saveChanges": "Wijzigingen opslaan", + "scheduleMode": "Downloadschema", + "scheduleModeCron": "Cron-expressie", + "scheduleModeDescription": "Downloaden via vaste intervallen o f Cron-expressies.", + "scheduleModeInterval": "Vast interval", "selectAccountType": "Selecteer accounttype", "selectAtLeastOneFolder": "Selecteer alstublieft ten minste één map", "selectAuthMethod": "Selecteer een authenticatiemethode", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP poort moet een positief geheel getal zijn", "incrementalSyncMustBeAtLeast10": "Incrementaal sync interval moet ten minste 10 minuten zijn", "incrementalSyncMustBeNumber": "Incrementaal sync interval moet een nummer zijn", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Ongeldig e-mailadres", "invalidUrl": "Ongeldige URL", "passwordMinLength": "Wachtwoord moet ten minste {{min}} tekens lang zijn", diff --git a/web/src/locales/no.json b/web/src/locales/no.json index 2f5adc3..b756b80 100644 --- a/web/src/locales/no.json +++ b/web/src/locales/no.json @@ -115,6 +115,24 @@ "continue": "Fortsett", "createdAt": "Opprettet", "creationFailed": "Opprettelse mislyktes, prøv igjen senere", + "cronAdvanced": "Avansert uttrykk", + "cronDaily": "Daglig", + "cronDayOfMonth": "Ugedag", + "cronDayOfWeek": "Ugedag", + "cronFrequency": "Frekvens", + "cronFriday": "Fredag", + "cronHour": "Time", + "cronMinute": "Minutt", + "cronMonday": "Mandag", + "cronMonthly": "Månedlig", + "cronSaturday": "Lørdag", + "cronSimple": "Enkelt uttrykk", + "cronSunday": "Søndag", + "cronThursday": "Torsdag", + "cronTimezoneNote": "Alle klokkeslett er serverens lokaltid.", + "cronTuesday": "Tirsdag", + "cronWednesday": "Onsdag", + "cronWeekly": "Ukentlig", "dateSelection": "Datovalg", "dateSince": "Dato siden", "days": "Dager", @@ -133,6 +151,9 @@ "downloadFailed": "Kunne ikke starte nedlastingsoppgave", "downloadInterval": "Nedlastingsintervall (minutter)", "downloadIntervalPlaceholder": "Skriv inn minutter", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-felts Cron (sek min time dag mdr ukedag) i servertid. Overstyrer intervall.", + "downloadSchedulePlaceholder": "f.eks.: 0 0 * * *", "downloadScope": "Nedlastingsstrategi", "downloadScopeDescription": "Velg hvilke e-poster som skal indekseres og lastes ned.", "downloadStarted": "Nedlastingsoppgave startet", @@ -247,6 +268,10 @@ } }, "saveChanges": "Lagre endringer", + "scheduleMode": "Nedlastingsplan", + "scheduleModeCron": "Cron-uttrykk", + "scheduleModeDescription": "Nedlasting med faste intervaller eller Cron-uttrykk.", + "scheduleModeInterval": "Fast intervall", "selectAccountType": "Velg kontotype", "selectAtLeastOneFolder": "Vennligst velg minst én mappe", "selectAuthMethod": "Velg en autentiseringsmetode", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP-port må være et positivt heltall", "incrementalSyncMustBeAtLeast10": "Intervall for inkrementell synkronisering må være minst 10 minutter", "incrementalSyncMustBeNumber": "Intervall for inkrementell synkronisering må være et tall", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Ugyldig e-postadresse", "invalidUrl": "Ugyldig URL", "passwordMinLength": "Passordet må være minst {{min}} tegn langt", diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json index 43e6f65..1b9c5ac 100644 --- a/web/src/locales/pl.json +++ b/web/src/locales/pl.json @@ -115,6 +115,24 @@ "continue": "Kontynuj", "createdAt": "Utworzono", "creationFailed": "Bład tworzenia, spróbuj później", + "cronAdvanced": "Zaawansowane wyrażenie", + "cronDaily": "Codziennie", + "cronDayOfMonth": "Dzień tygodnia", + "cronDayOfWeek": "Dzień tygodnia", + "cronFrequency": "Częstotliwość", + "cronFriday": "Piątek", + "cronHour": "Godzina", + "cronMinute": "Minuta", + "cronMonday": "Poniedziałek", + "cronMonthly": "Co miesiąc", + "cronSaturday": "Sobota", + "cronSimple": "Proste wyrażenie", + "cronSunday": "Niedziela", + "cronThursday": "Czwartek", + "cronTimezoneNote": "Czas według lokalnej strefy serwera.", + "cronTuesday": "Wtorek", + "cronWednesday": "Środa", + "cronWeekly": "Co tydzień", "dateSelection": "Zaznaczenie daty", "dateSince": "Od kiedy", "days": "Dni", @@ -133,6 +151,9 @@ "downloadFailed": "Nie udało się uruchomić zadania pobierania", "downloadInterval": "Cykl pobierania (minuty)", "downloadIntervalPlaceholder": "Wprowadź minuty", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-polowy Cron (sek min godz dz msc dz-tyg) w czasie serwera. Nadpisuje interwał.", + "downloadSchedulePlaceholder": "np. 0 0 * * *", "downloadScope": "Strategia pobierania", "downloadScopeDescription": "Wybierz, które wiadomości e-mail mają być indeksowane i pobierane.", "downloadStarted": "Uruchomiono zadanie pobierania", @@ -247,6 +268,10 @@ } }, "saveChanges": "Zapisz zmiany", + "scheduleMode": "Harmonogram pobierania", + "scheduleModeCron": "Wyrażenie Cron", + "scheduleModeDescription": "Pobieranie w stałych odstępach lub przez wyrażenia Cron.", + "scheduleModeInterval": "Stały odstęp", "selectAccountType": "Wybierz typ konta", "selectAtLeastOneFolder": "Wybierz co najmniej jeden folder", "selectAuthMethod": "Zaznacz metodę uwierzytelniania IMAP", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "Port IMAP musi być liczbą całkowitą dodatnią", "incrementalSyncMustBeAtLeast10": "Przyrostowy interwał synchronizacji musi wynosić co najmniej 10 minut", "incrementalSyncMustBeNumber": "Przyrostowy interwał synchronizacji musi być liczbą", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Niewłaściwy adres email", "invalidUrl": "Niewłaściwy URL", "passwordMinLength": "Hasło musi posiadać conajmniej {{min}} znaków", diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index 7de4c29..161197a 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -115,6 +115,24 @@ "continue": "Continuar", "createdAt": "Criado Em", "creationFailed": "Falha na criação, por favor, tente novamente mais tarde.", + "cronAdvanced": "Expressão avançada", + "cronDaily": "Diariamente", + "cronDayOfMonth": "Dia da semana", + "cronDayOfWeek": "Dia da semana", + "cronFrequency": "Frequência", + "cronFriday": "Sexta-feira", + "cronHour": "Hora", + "cronMinute": "Minuto", + "cronMonday": "Segunda-feira", + "cronMonthly": "Mensalmente", + "cronSaturday": "Sábado", + "cronSimple": "Expressão simples", + "cronSunday": "Domingo", + "cronThursday": "Quinta-feira", + "cronTimezoneNote": "Horários no fuso horário do servidor.", + "cronTuesday": "Terça-feira", + "cronWednesday": "Quarta-feira", + "cronWeekly": "Semanalmente", "dateSelection": "Seleção de Data", "dateSince": "Data de Início da Sincronização", "days": "Dias", @@ -133,6 +151,9 @@ "downloadFailed": "Falha ao iniciar tarefa de download", "downloadInterval": "Intervalo de download (minutos)", "downloadIntervalPlaceholder": "Insira os minutos", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "Cron de 6 campos (seg min hora dia mês dia-sem) na hora do servidor. Substitui o intervalo.", + "downloadSchedulePlaceholder": "ex: 0 0 * * *", "downloadScope": "Estratégia de download", "downloadScopeDescription": "Escolha quais e-mails devem ser indexados e baixados.", "downloadStarted": "Tarefa de download iniciada", @@ -247,6 +268,10 @@ } }, "saveChanges": "Salvar Alterações", + "scheduleMode": "Agendamento de download", + "scheduleModeCron": "Expressão Cron", + "scheduleModeDescription": "Download por intervalos fixos ou expressões Cron.", + "scheduleModeInterval": "Intervalo fixo", "selectAccountType": "Selecionar tipo de conta", "selectAtLeastOneFolder": "Por favor, selecione pelo menos uma pasta", "selectAuthMethod": "Selecionar Método de Autenticação", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "A Porta IMAP deve ser um número inteiro positivo", "incrementalSyncMustBeAtLeast10": "O Intervalo de Sincronização Incremental deve ser de pelo menos 10 minutos", "incrementalSyncMustBeNumber": "O Intervalo de Sincronização Incremental deve ser um número", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Endereço de email inválido", "invalidUrl": "URL inválido", "passwordMinLength": "A senha deve ter pelo menos {{min}} caracteres", diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index 9b1169d..5acaf3f 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -115,6 +115,24 @@ "continue": "Продолжить", "createdAt": "Создано", "creationFailed": "Ошибка создания, попробуйте позже", + "cronAdvanced": "Расширенное выражение", + "cronDaily": "Ежедневно", + "cronDayOfMonth": "День недели", + "cronDayOfWeek": "День недели", + "cronFrequency": "Частота", + "cronFriday": "Пятница", + "cronHour": "Час", + "cronMinute": "Минута", + "cronMonday": "Понедельник", + "cronMonthly": "Ежемесячно", + "cronSaturday": "Суббота", + "cronSimple": "Простое выражение", + "cronSunday": "Воскресенье", + "cronThursday": "Четверг", + "cronTimezoneNote": "Время по местному часовому поясу сервера.", + "cronTuesday": "Вторник", + "cronWednesday": "Среда", + "cronWeekly": "Еженедельно", "dateSelection": "Выбор даты", "dateSince": "Дата с", "days": "Дни", @@ -133,6 +151,9 @@ "downloadFailed": "Не удалось запустить задачу загрузки", "downloadInterval": "Интервал загрузки (мин.)", "downloadIntervalPlaceholder": "Введите минуты", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-польный Cron (сек мин час день мес день-нед) по времени сервера. Меняет интервал.", + "downloadSchedulePlaceholder": "напр., 0 0 * * *", "downloadScope": "Стратегия загрузки", "downloadScopeDescription": "Выберите, какие электронные письма должны быть проиндексированы и скачаны.", "downloadStarted": "Задача загрузки запущена", @@ -247,6 +268,10 @@ } }, "saveChanges": "Сохранить изменения", + "scheduleMode": "Расписание загрузки", + "scheduleModeCron": "Выражение Cron", + "scheduleModeDescription": "Загрузка с фиксированным интервалом или по Cron.", + "scheduleModeInterval": "Фиксированный интервал", "selectAccountType": "Выберите тип аккаунта", "selectAtLeastOneFolder": "Пожалуйста, выберите хотя бы одну папку", "selectAuthMethod": "Выберите метод авторизации", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP порт должен быть положительным целым числом", "incrementalSyncMustBeAtLeast10": "Интервал инкрементальной синхронизации должен быть не менее 10 минут", "incrementalSyncMustBeNumber": "Интервал инкрементальной синхронизации должен быть числом", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Неверный адрес электронной почты", "invalidUrl": "Неверный URL", "passwordMinLength": "Пароль должен быть не менее {{min}} символов", diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json index 50a4c48..bc0ebb5 100644 --- a/web/src/locales/sv.json +++ b/web/src/locales/sv.json @@ -115,6 +115,24 @@ "continue": "Fortsätt", "createdAt": "Skapad", "creationFailed": "Skapande misslyckades, försök igen senare", + "cronAdvanced": "Avancerat uttryck", + "cronDaily": "Dagligen", + "cronDayOfMonth": "Veckodag", + "cronDayOfWeek": "Veckodag", + "cronFrequency": "Frekvens", + "cronFriday": "Fredag", + "cronHour": "Timme", + "cronMinute": "Minut", + "cronMonday": "Måndag", + "cronMonthly": "Månadsvis", + "cronSaturday": "Lördag", + "cronSimple": "Enkelt uttryck", + "cronSunday": "Söndag", + "cronThursday": "Torsdag", + "cronTimezoneNote": "Alla tider visas i serverns lokaltid.", + "cronTuesday": "Tisdag", + "cronWednesday": "Onsdag", + "cronWeekly": "Veckovis", "dateSelection": "Datumsval", "dateSince": "Datum från", "days": "Dagar", @@ -133,6 +151,9 @@ "downloadFailed": "Misslyckades med att starta hämtningsuppgift", "downloadInterval": "Nedladdningsintervall (minuter)", "downloadIntervalPlaceholder": "Ange minuter", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "6-fälts Cron (sek min tim dag mån veckodag) i servertid. Ersätter intervall.", + "downloadSchedulePlaceholder": "t.ex. 0 0 * * *", "downloadScope": "Nedladdningsstrategi", "downloadScopeDescription": "Välj vilka e-postmeddelanden som ska indexeras och laddas ner.", "downloadStarted": "Hämtningsuppgift startad", @@ -247,6 +268,10 @@ } }, "saveChanges": "Spara ändringar", + "scheduleMode": "Hämtningsschema", + "scheduleModeCron": "Cron-uttryck", + "scheduleModeDescription": "Hämta med fasta intervall eller Cron-uttryck.", + "scheduleModeInterval": "Fast intervall", "selectAccountType": "Välj kontotyp", "selectAtLeastOneFolder": "Vänligen välj minst en mapp", "selectAuthMethod": "Välj en autentiseringsmetod", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP-port måste vara ett positivt heltal", "incrementalSyncMustBeAtLeast10": "Intervall för inkrementell synkronisering måste vara minst 10 minuter", "incrementalSyncMustBeNumber": "Intervall för inkrementell synkronisering måste vara ett tal", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "Ogiltig e-postadress", "invalidUrl": "Ogiltig URL", "passwordMinLength": "Lösenordet måste vara minst {{min}} tecken långt", diff --git a/web/src/locales/zh-tw.json b/web/src/locales/zh-tw.json index 0c3271b..fbc5d33 100644 --- a/web/src/locales/zh-tw.json +++ b/web/src/locales/zh-tw.json @@ -115,6 +115,24 @@ "continue": "繼續", "createdAt": "建立時間", "creationFailed": "建立失敗,請稍後再試。", + "cronAdvanced": "高級表達式", + "cronDaily": "每天", + "cronDayOfMonth": "星期", + "cronDayOfWeek": "星期", + "cronFrequency": "頻率", + "cronFriday": "星期五", + "cronHour": "小時", + "cronMinute": "分鐘", + "cronMonday": "星期一", + "cronMonthly": "每月", + "cronSaturday": "星期六", + "cronSimple": "简易表達式", + "cronSunday": "星期日", + "cronThursday": "星期四", + "cronTimezoneNote": "所有時間均使用伺服器在地時區。", + "cronTuesday": "星期二", + "cronWednesday": "星期三", + "cronWeekly": "每周", "dateSelection": "日期選擇", "dateSince": "同步起始日期", "days": "天", @@ -133,6 +151,9 @@ "downloadFailed": "啟動下載任務失敗", "downloadInterval": "下載週期 (分鐘)", "downloadIntervalPlaceholder": "請輸入分鐘數", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "伺服器時間的 6 位 Cron 表達式(秒分時日月週),提供時覆蓋間隔設定。", + "downloadSchedulePlaceholder": "例如:0 0 * * *", "downloadScope": "下載策略", "downloadScopeDescription": "選擇哪些郵件應被索引和下載。", "downloadStarted": "下載任務已啟動", @@ -247,6 +268,10 @@ } }, "saveChanges": "儲存變更", + "scheduleMode": "下載排程", + "scheduleModeCron": "Cron 表達式", + "scheduleModeDescription": "設定固定間隔或 Cron 表達式下載郵件。", + "scheduleModeInterval": "固定間隔", "selectAccountType": "選擇郵件帳戶類型", "selectAtLeastOneFolder": "請至少選擇一個資料夾", "selectAuthMethod": "選擇驗證方法", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP 連接埠必須是正整數", "incrementalSyncMustBeAtLeast10": "增量同步間隔必須至少為 10 分鐘", "incrementalSyncMustBeNumber": "增量同步間隔必須是數字", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "無效的電子郵件地址", "invalidUrl": "無效的網址", "passwordMinLength": "密碼長度必須至少 {{min}} 個字元", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 9b71302..ca170d5 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -115,6 +115,24 @@ "continue": "继续", "createdAt": "创建时间", "creationFailed": "创建失败,请稍后重试", + "cronAdvanced": "高级表达式", + "cronDaily": "每天", + "cronDayOfMonth": "星期", + "cronDayOfWeek": "星期", + "cronFrequency": "频率", + "cronFriday": "星期五", + "cronHour": "小时", + "cronMinute": "分钟", + "cronMonday": "星期一", + "cronMonthly": "每月", + "cronSaturday": "星期六", + "cronSimple": "简易表达式", + "cronSunday": "星期日", + "cronThursday": "星期四", + "cronTimezoneNote": "所有时间均使用服务器本地时区。", + "cronTuesday": "星期二", + "cronWednesday": "星期三", + "cronWeekly": "每周", "dateSelection": "日期选择", "dateSince": "起始日期", "days": "天", @@ -133,6 +151,9 @@ "downloadFailed": "启动下载任务失败", "downloadInterval": "下载周期 (分钟)", "downloadIntervalPlaceholder": "请输入分钟数", + "downloadSchedule": "Cron Schedule", + "downloadScheduleDescription": "服务器时间的 6 位 Cron 表达式(秒分时日月周),提供时覆盖间隔设置。", + "downloadSchedulePlaceholder": "例如:0 0 * * *", "downloadScope": "下载策略", "downloadScopeDescription": "选择哪些邮件应被索引和下载。", "downloadStarted": "下载任务已启动", @@ -247,6 +268,10 @@ } }, "saveChanges": "保存更改", + "scheduleMode": "下载调度", + "scheduleModeCron": "Cron 表达式", + "scheduleModeDescription": "设置固定间隔或 Cron 表达式下载邮件。", + "scheduleModeInterval": "固定间隔", "selectAccountType": "选择邮件账户类型", "selectAtLeastOneFolder": "请至少选择一个文件夹", "selectAuthMethod": "选择认证方法", @@ -1658,6 +1683,7 @@ "imapPortMustBePositive": "IMAP 端口必须是正整数", "incrementalSyncMustBeAtLeast10": "增量同步间隔必须至少为 10 分钟", "incrementalSyncMustBeNumber": "增量同步间隔必须是数字", + "invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')", "invalidEmail": "无效的电子邮件地址", "invalidUrl": "无效的 URL", "passwordMinLength": "密码长度至少为 {{min}} 个字符",