From 6e7347da65a60f1661cb58f673e1fea693827b0a Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sat, 4 Apr 2026 20:49:47 +0100 Subject: [PATCH 01/29] Fix Google Calendar debounce ignoring fresh MCP task payload --- src/services/TaskCalendarSyncService.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/services/TaskCalendarSyncService.ts b/src/services/TaskCalendarSyncService.ts index e0adc342f..439229d82 100644 --- a/src/services/TaskCalendarSyncService.ts +++ b/src/services/TaskCalendarSyncService.ts @@ -19,6 +19,8 @@ const GOOGLE_API_CALL_SPACING_MS = 100; /** * Service for syncing TaskNotes tasks to Google Calendar. * Handles creating, updating, and deleting calendar events when tasks change. + * + * Patched locally: __codexTaskCalendarDebounceStaleCachePatch20260404 */ export class TaskCalendarSyncService { private plugin: TaskNotesPlugin; @@ -35,6 +37,9 @@ export class TaskCalendarSyncService { /** Track previous task state for detecting recurrence removal */ private previousTaskState: Map = new Map(); + /** Store the latest explicitly passed task object during debounce to avoid cache race conditions */ + private pendingTasks: Map = new Map(); + constructor(plugin: TaskNotesPlugin, googleCalendarService: GoogleCalendarService) { this.plugin = plugin; this.googleCalendarService = googleCalendarService; @@ -49,6 +54,7 @@ export class TaskCalendarSyncService { } this.pendingSyncs.clear(); this.previousTaskState.clear(); + this.pendingTasks.clear(); } /** @@ -765,6 +771,9 @@ export class TaskCalendarSyncService { clearTimeout(existingTimer); } + // Store the authoritative task state passed to us so we don't rely on the async metadata cache + this.pendingTasks.set(taskPath, task); + // Return a promise that resolves when the debounced sync completes return new Promise((resolve, reject) => { const timer = setTimeout(async () => { @@ -776,8 +785,13 @@ export class TaskCalendarSyncService { await inFlight.catch(() => {}); // Ignore errors from previous sync } - // Re-fetch the task to get the latest state after debounce - const freshTask = await this.plugin.cacheManager.getTaskInfo(taskPath); + // Use the latest task data that was passed to us explicitly + const latestTask = this.pendingTasks.get(taskPath); + this.pendingTasks.delete(taskPath); + + // Fallback to cache ONLY if for some reason the pending task is missing + const freshTask = latestTask || await this.plugin.cacheManager.getTaskInfo(taskPath); + if (!freshTask) { resolve(); return; From 35620abece228104a771c550d805a28c69b5846d Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sat, 4 Apr 2026 20:59:15 +0100 Subject: [PATCH 02/29] Add unit test for TaskCalendarSyncService task debounce explicit caching --- .../services/TaskCalendarSyncService.test.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/services/TaskCalendarSyncService.test.ts diff --git a/tests/services/TaskCalendarSyncService.test.ts b/tests/services/TaskCalendarSyncService.test.ts new file mode 100644 index 000000000..68d400833 --- /dev/null +++ b/tests/services/TaskCalendarSyncService.test.ts @@ -0,0 +1,74 @@ +import { TaskCalendarSyncService } from "../../src/services/TaskCalendarSyncService"; +import { TaskInfo } from "../../src/types"; + +describe("TaskCalendarSyncService", () => { + let syncService: any; + let mockPlugin: any; + let mockGoogleCalendarService: any; + + beforeEach(() => { + mockPlugin = { + settings: { + googleCalendarExport: { + syncOnTaskUpdate: true, + targetCalendarId: "test-calendar", + } + }, + cacheManager: { + getTaskInfo: jest.fn() + }, + statusManager: { + getStatusConfig: jest.fn().mockReturnValue({ label: "Todo" }) + }, + priorityManager: { + getPriorityConfig: jest.fn().mockReturnValue({ label: "High" }) + }, + i18n: { + translate: jest.fn().mockReturnValue("Untitled Task") + } + }; + + mockGoogleCalendarService = { + updateEvent: jest.fn().mockResolvedValue({}), + createEvent: jest.fn().mockResolvedValue({ id: "test-id" }) + }; + + syncService = new TaskCalendarSyncService(mockPlugin, mockGoogleCalendarService); + + // Mock internal methods to avoid testing downstream serialization logic which might be complex + syncService.executeTaskUpdate = jest.fn().mockResolvedValue(undefined); + }); + + it("should use the most recently passed task explicitly, avoiding stale cacheManager payloads during debounce", async () => { + const taskPath = "test/path.md"; + + const firstPayload: TaskInfo = { + path: taskPath, + title: "Task Title", + scheduled: "2026-04-04" + }; + + const secondPayload: TaskInfo = { + path: taskPath, + title: "Task Title", + scheduled: "2026-04-06" // Agent updated it to April 6 + }; + + // Pretend the metadataCache hasn't caught up and still returns the stale task + mockPlugin.cacheManager.getTaskInfo.mockResolvedValue(firstPayload); + + // Act: trigger sync twice rapidly to simulate MCP updates or user typing + syncService.updateTaskInCalendar(firstPayload); + syncService.updateTaskInCalendar(secondPayload); + + // Fast-forward the 500ms debounce + await new Promise(r => setTimeout(r, 600)); + + // Let the event loop flush the internal promises + await new Promise(process.nextTick); + + // Assert: It should execute only once, and pass the explicit secondPayload, not the stale cache! + expect(syncService.executeTaskUpdate).toHaveBeenCalledTimes(1); + expect(syncService.executeTaskUpdate).toHaveBeenCalledWith(secondPayload); + }); +}); From ef84eaefcca9eeb2cea65ceb02d463a5221d2a64 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sat, 4 Apr 2026 23:34:56 +0100 Subject: [PATCH 03/29] Use fake timers in debounce cache test to avoid CI flakiness Co-Authored-By: Claude Opus 4.6 --- .../services/TaskCalendarSyncService.test.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/services/TaskCalendarSyncService.test.ts b/tests/services/TaskCalendarSyncService.test.ts index 68d400833..21c0dd4be 100644 --- a/tests/services/TaskCalendarSyncService.test.ts +++ b/tests/services/TaskCalendarSyncService.test.ts @@ -7,6 +7,8 @@ describe("TaskCalendarSyncService", () => { let mockGoogleCalendarService: any; beforeEach(() => { + jest.useFakeTimers(); + mockPlugin = { settings: { googleCalendarExport: { @@ -34,20 +36,24 @@ describe("TaskCalendarSyncService", () => { }; syncService = new TaskCalendarSyncService(mockPlugin, mockGoogleCalendarService); - + // Mock internal methods to avoid testing downstream serialization logic which might be complex syncService.executeTaskUpdate = jest.fn().mockResolvedValue(undefined); }); + afterEach(() => { + jest.useRealTimers(); + }); + it("should use the most recently passed task explicitly, avoiding stale cacheManager payloads during debounce", async () => { const taskPath = "test/path.md"; - + const firstPayload: TaskInfo = { path: taskPath, title: "Task Title", scheduled: "2026-04-04" }; - + const secondPayload: TaskInfo = { path: taskPath, title: "Task Title", @@ -61,11 +67,12 @@ describe("TaskCalendarSyncService", () => { syncService.updateTaskInCalendar(firstPayload); syncService.updateTaskInCalendar(secondPayload); - // Fast-forward the 500ms debounce - await new Promise(r => setTimeout(r, 600)); + // Fast-forward past the 500ms debounce + jest.advanceTimersByTime(500); - // Let the event loop flush the internal promises - await new Promise(process.nextTick); + // Flush the microtask queue so the async debounce handler completes + await Promise.resolve(); + await Promise.resolve(); // Assert: It should execute only once, and pass the explicit secondPayload, not the stale cache! expect(syncService.executeTaskUpdate).toHaveBeenCalledTimes(1); From 979980e6a320a77feda291078d304ad30e3f7dbf Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 5 Apr 2026 01:01:42 +0100 Subject: [PATCH 04/29] Fix recurring late completion defaulting to today instead of scheduled date (#396) toggleRecurringTaskComplete and toggleRecurringTaskSkipped now default to the task's scheduled occurrence date for scheduled-anchor recurring tasks when no explicit date is passed, instead of unconditionally using today. --- src/services/TaskService.ts | 15 ++++- ...curring-late-completion-wrong-date.test.ts | 65 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts index c34e6c717..e66750d65 100644 --- a/src/services/TaskService.ts +++ b/src/services/TaskService.ts @@ -39,10 +39,12 @@ import { import { getProjectDisplayName } from "../utils/linkUtils"; import { formatDateForStorage, + getDatePart, getCurrentDateString, getCurrentTimestamp, getTodayLocal, createUTCDateFromLocalCalendarDate, + parseDateToUTC, } from "../utils/dateUtils"; import { format } from "date-fns"; import { processFolderTemplate, TaskTemplateData } from "../utils/folderTemplateProcessor"; @@ -1288,11 +1290,14 @@ export class TaskService { throw new Error("Task is not recurring"); } - // Default to local today instead of selectedDate for recurring task completion - // This ensures completion is recorded for user's actual calendar day unless explicitly overridden + // For scheduled-anchor recurring tasks, default to the scheduled occurrence + // date — not today — so late completions mark the correct instance (#396) const targetDate = date || (() => { + if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { + return parseDateToUTC(getDatePart(freshTask.scheduled)); + } const todayLocal = getTodayLocal(); return createUTCDateFromLocalCalendarDate(todayLocal); })(); @@ -1514,10 +1519,14 @@ export class TaskService { throw new Error("Task is not recurring"); } - // Default to local today + // For scheduled-anchor recurring tasks, default to the scheduled occurrence + // date — not today — so late skips mark the correct instance (#396) const targetDate = date || (() => { + if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { + return parseDateToUTC(getDatePart(freshTask.scheduled)); + } const todayLocal = getTodayLocal(); return createUTCDateFromLocalCalendarDate(todayLocal); })(); diff --git a/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts b/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts new file mode 100644 index 000000000..ac2af4b8b --- /dev/null +++ b/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts @@ -0,0 +1,65 @@ +/** + * Issue #396: Recurring tasks completed after scheduled date do not process + * into next available recurrence. + * + * Root cause: toggleRecurringTaskComplete defaults to getTodayLocal() when no + * explicit date is passed. For scheduled-anchor recurring tasks, this records + * today in complete_instances instead of the scheduled occurrence date. + * + * Fix: default to task.scheduled (via getDatePart) for scheduled-anchor tasks. + */ + +import { getDatePart, parseDateToUTC } from "../../../src/utils/dateUtils"; + +describe("Issue #396 — recurring late completion records wrong date", () => { + it("getDatePart extracts date from scheduled datetime", () => { + expect(getDatePart("2026-04-04")).toBe("2026-04-04"); + expect(getDatePart("2026-04-04T10:00:00")).toBe("2026-04-04"); + }); + + it("scheduled-anchor task should use scheduled date, not today, when date is omitted", () => { + // Simulate the fixed defaulting logic + const freshTask = { + recurrence_anchor: "scheduled", + scheduled: "2026-04-04", + recurrence: "FREQ=WEEKLY;BYDAY=SA", + }; + + // The fix: when date is omitted and anchor is not "completion", use scheduled + const resolvedDate = (() => { + if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { + return parseDateToUTC(getDatePart(freshTask.scheduled)); + } + fail("Should not reach today fallback for scheduled-anchor task"); + })(); + + // The resolved date should represent April 4, not whatever today happens to be + expect(resolvedDate.toISOString()).toMatch(/^2026-04-04/); + }); + + it("completion-anchor task should still default to today when date is omitted", () => { + const freshTask = { + recurrence_anchor: "completion", + scheduled: "2026-04-04", + recurrence: "FREQ=WEEKLY;BYDAY=SA", + }; + + // The fix should NOT change completion-anchor behavior + const usedScheduled = + freshTask.recurrence_anchor !== "completion" && freshTask.scheduled; + expect(usedScheduled).toBeFalsy(); + }); + + it("undefined recurrence_anchor should default to using scheduled date", () => { + // Default anchor is "scheduled" — undefined should behave the same + const freshTask = { + recurrence_anchor: undefined, + scheduled: "2026-04-04", + recurrence: "FREQ=WEEKLY;BYDAY=SA", + }; + + const usedScheduled = + freshTask.recurrence_anchor !== "completion" && freshTask.scheduled; + expect(usedScheduled).toBeTruthy(); + }); +}); From 7506c59b69d704f32c50f977360e64d9e6def06f Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 5 Apr 2026 03:03:27 +0100 Subject: [PATCH 05/29] Fix main.ts wrapper using stale date for completion notice (#396) The plugin-level toggleRecurringTaskComplete wrapper independently derived the completion date from getTodayLocal(), diverging from the TaskService fix. Now uses the same scheduled-anchor resolution. Also replaced pseudo-test with a behavioural test that exercises the real TaskService.toggleRecurringTaskComplete method with mocked dependencies. --- src/main.ts | 8 +- ...curring-late-completion-wrong-date.test.ts | 156 +++++++++++++----- 2 files changed, 121 insertions(+), 43 deletions(-) diff --git a/src/main.ts b/src/main.ts index adb711fdc..ca7dd7cf7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,7 @@ import { TaskEditModal } from "./modals/TaskEditModal"; import { openTaskSelector } from "./modals/TaskSelectorWithCreateModal"; import { PomodoroService } from "./services/PomodoroService"; import { formatTime, getActiveTimeEntry } from "./utils/helpers"; -import { convertUTCToLocalCalendarDate, getCurrentTimestamp } from "./utils/dateUtils"; +import { convertUTCToLocalCalendarDate, getCurrentTimestamp, getDatePart, parseDateToUTC } from "./utils/dateUtils"; import { TaskManager } from "./utils/TaskManager"; import { DependencyCache } from "./utils/DependencyCache"; import { RequestDeduplicator, PredictivePrefetcher } from "./utils/RequestDeduplicator"; @@ -1063,11 +1063,13 @@ export default class TaskNotesPlugin extends Plugin { // Let TaskService handle the date logic (defaults to local today, not selectedDate) const updatedTask = await this.taskService.toggleRecurringTaskComplete(task, date); - // For notification, determine the actual completion date from the task - // Use local today if no explicit date provided + // Use the same implicit-date resolution as TaskService (#396) const targetDate = date || (() => { + if (task.recurrence_anchor !== "completion" && task.scheduled) { + return parseDateToUTC(getDatePart(task.scheduled)); + } const todayLocal = getTodayLocal(); return createUTCDateFromLocalCalendarDate(todayLocal); })(); diff --git a/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts b/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts index ac2af4b8b..33d665f23 100644 --- a/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts +++ b/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts @@ -9,57 +9,133 @@ * Fix: default to task.scheduled (via getDatePart) for scheduled-anchor tasks. */ -import { getDatePart, parseDateToUTC } from "../../../src/utils/dateUtils"; +import { TFile } from "obsidian"; +import { TaskService } from "../../../src/services/TaskService"; +import { TaskInfo } from "../../../src/types"; +import { TaskFactory } from "../../helpers/mock-factories"; +import { getTodayLocal } from "../../../src/utils/dateUtils"; + +// Mock dateUtils so we can control "today" +jest.mock("../../../src/utils/dateUtils", () => ({ + ...jest.requireActual("../../../src/utils/dateUtils"), + getTodayLocal: jest.fn(), +})); + +const mockGetTodayLocal = getTodayLocal as jest.MockedFunction; describe("Issue #396 — recurring late completion records wrong date", () => { - it("getDatePart extracts date from scheduled datetime", () => { - expect(getDatePart("2026-04-04")).toBe("2026-04-04"); - expect(getDatePart("2026-04-04T10:00:00")).toBe("2026-04-04"); - }); + let taskService: TaskService; + let writtenFrontmatter: Record; - it("scheduled-anchor task should use scheduled date, not today, when date is omitted", () => { - // Simulate the fixed defaulting logic - const freshTask = { - recurrence_anchor: "scheduled", - scheduled: "2026-04-04", + function buildMockPlugin(task: TaskInfo) { + writtenFrontmatter = {}; + return { + app: { + vault: { + getAbstractFileByPath: jest.fn().mockReturnValue(new TFile(task.path)), + modify: jest.fn(), + read: jest.fn().mockResolvedValue(""), + }, + workspace: { getActiveFile: jest.fn() }, + metadataCache: { getCache: jest.fn() }, + fileManager: { + processFrontMatter: jest.fn().mockImplementation((_file: any, fn: any) => { + // Start with empty frontmatter like Obsidian does + const fm: Record = {}; + fn(fm); + Object.assign(writtenFrontmatter, fm); + return Promise.resolve(); + }), + }, + }, + settings: { + taskFolder: "tasks", + fieldMapping: {}, + defaultTaskStatus: "open", + taskTag: "#task", + storeTitleInFilename: false, + resetCheckboxesOnRecurrence: false, + }, + statusManager: { + isCompletedStatus: jest.fn((s: string) => s === "done"), + getCompletedStatuses: jest.fn(() => ["done"]), + }, + // Return field names as-is — the test checks the mapped field name + fieldMapper: { toUserField: jest.fn((f: string) => f) }, + cacheManager: { + getTaskInfo: jest.fn().mockResolvedValue(task), + updateTaskInfoInCache: jest.fn(), + waitForFreshTaskData: jest.fn().mockResolvedValue(undefined), + }, + emitter: { trigger: jest.fn() }, + } as any; + } + + it("scheduled-anchor task completed late records the scheduled date, not today", async () => { + // Saturday task, completed on Sunday + mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday + + const saturdayTask = TaskFactory.createTask({ + path: "tasks/test.md", + title: "Weekly task", recurrence: "FREQ=WEEKLY;BYDAY=SA", - }; - - // The fix: when date is omitted and anchor is not "completion", use scheduled - const resolvedDate = (() => { - if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { - return parseDateToUTC(getDatePart(freshTask.scheduled)); - } - fail("Should not reach today fallback for scheduled-anchor task"); - })(); - - // The resolved date should represent April 4, not whatever today happens to be - expect(resolvedDate.toISOString()).toMatch(/^2026-04-04/); + recurrence_anchor: "scheduled", + scheduled: "2026-04-04", // Saturday + complete_instances: [], + }); + + const plugin = buildMockPlugin(saturdayTask); + taskService = new TaskService(plugin); + + // Call WITHOUT explicit date — should default to scheduled, not today + await taskService.toggleRecurringTaskComplete(saturdayTask); + + // completeInstances is the mapped field name (toUserField returns as-is) + expect(writtenFrontmatter.completeInstances).toContain("2026-04-04"); + expect(writtenFrontmatter.completeInstances).not.toContain("2026-04-05"); }); - it("completion-anchor task should still default to today when date is omitted", () => { - const freshTask = { - recurrence_anchor: "completion", - scheduled: "2026-04-04", + it("completion-anchor task completed late records today, not the scheduled date", async () => { + mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday + + const completionTask = TaskFactory.createTask({ + path: "tasks/test.md", + title: "Completion-anchor task", recurrence: "FREQ=WEEKLY;BYDAY=SA", - }; + recurrence_anchor: "completion", + scheduled: "2026-04-04", // Saturday + complete_instances: [], + }); + + const plugin = buildMockPlugin(completionTask); + taskService = new TaskService(plugin); - // The fix should NOT change completion-anchor behavior - const usedScheduled = - freshTask.recurrence_anchor !== "completion" && freshTask.scheduled; - expect(usedScheduled).toBeFalsy(); + await taskService.toggleRecurringTaskComplete(completionTask); + + // completion-anchor should use today (Sunday), not scheduled (Saturday) + expect(writtenFrontmatter.completeInstances).toContain("2026-04-05"); + expect(writtenFrontmatter.completeInstances).not.toContain("2026-04-04"); }); - it("undefined recurrence_anchor should default to using scheduled date", () => { - // Default anchor is "scheduled" — undefined should behave the same - const freshTask = { - recurrence_anchor: undefined, - scheduled: "2026-04-04", + it("undefined recurrence_anchor defaults to using the scheduled date", async () => { + mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday + + const defaultTask = TaskFactory.createTask({ + path: "tasks/test.md", + title: "Default anchor task", recurrence: "FREQ=WEEKLY;BYDAY=SA", - }; + scheduled: "2026-04-04", // Saturday + complete_instances: [], + }); + // Ensure recurrence_anchor is undefined (default = scheduled) + delete (defaultTask as any).recurrence_anchor; + + const plugin = buildMockPlugin(defaultTask); + taskService = new TaskService(plugin); + + await taskService.toggleRecurringTaskComplete(defaultTask); - const usedScheduled = - freshTask.recurrence_anchor !== "completion" && freshTask.scheduled; - expect(usedScheduled).toBeTruthy(); + expect(writtenFrontmatter.completeInstances).toContain("2026-04-04"); + expect(writtenFrontmatter.completeInstances).not.toContain("2026-04-05"); }); }); From f7dfd7b1aec0474501eac85c120654b48c4a64bd Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 5 Apr 2026 03:16:30 +0100 Subject: [PATCH 06/29] Use authoritative cache state for completion notice, add skip path tests (#396) The main.ts wrapper was resolving the notice date from the caller's potentially stale task object. Now reads from cacheManager before the service call, matching the authoritative source the service uses. Added behavioural regression tests for toggleRecurringTaskSkipped to match the existing completion path coverage. Co-Authored-By: Claude Opus 4.6 --- src/main.ts | 13 +- ...curring-late-completion-wrong-date.test.ts | 197 ++++++++++++------ 2 files changed, 137 insertions(+), 73 deletions(-) diff --git a/src/main.ts b/src/main.ts index ca7dd7cf7..1764bd11c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1060,20 +1060,21 @@ export default class TaskNotesPlugin extends Plugin { */ async toggleRecurringTaskComplete(task: TaskInfo, date?: Date): Promise { try { - // Let TaskService handle the date logic (defaults to local today, not selectedDate) - const updatedTask = await this.taskService.toggleRecurringTaskComplete(task, date); - - // Use the same implicit-date resolution as TaskService (#396) + // Resolve the implicit date from cacheManager — the same authoritative + // source the service uses — before the service mutates state (#396) + const freshTask = (await this.cacheManager.getTaskInfo(task.path)) || task; const targetDate = date || (() => { - if (task.recurrence_anchor !== "completion" && task.scheduled) { - return parseDateToUTC(getDatePart(task.scheduled)); + if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { + return parseDateToUTC(getDatePart(freshTask.scheduled)); } const todayLocal = getTodayLocal(); return createUTCDateFromLocalCalendarDate(todayLocal); })(); + const updatedTask = await this.taskService.toggleRecurringTaskComplete(task, date); + const dateStr = formatDateForStorage(targetDate); const wasCompleted = updatedTask.complete_instances?.includes(dateStr); const action = wasCompleted ? "completed" : "marked incomplete"; diff --git a/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts b/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts index 33d665f23..53ce2fc52 100644 --- a/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts +++ b/tests/unit/issues/issue-396-recurring-late-completion-wrong-date.test.ts @@ -1,10 +1,12 @@ /** - * Issue #396: Recurring tasks completed after scheduled date do not process - * into next available recurrence. + * Issue #396: Recurring tasks completed/skipped after scheduled date do not + * process into next available recurrence. * - * Root cause: toggleRecurringTaskComplete defaults to getTodayLocal() when no - * explicit date is passed. For scheduled-anchor recurring tasks, this records - * today in complete_instances instead of the scheduled occurrence date. + * Root cause: toggleRecurringTaskComplete and toggleRecurringTaskSkipped + * default to getTodayLocal() when no explicit date is passed. For + * scheduled-anchor recurring tasks, this records today in + * complete_instances / skipped_instances instead of the scheduled + * occurrence date. * * Fix: default to task.scheduled (via getDatePart) for scheduled-anchor tasks. */ @@ -23,59 +25,55 @@ jest.mock("../../../src/utils/dateUtils", () => ({ const mockGetTodayLocal = getTodayLocal as jest.MockedFunction; -describe("Issue #396 — recurring late completion records wrong date", () => { - let taskService: TaskService; - let writtenFrontmatter: Record; - - function buildMockPlugin(task: TaskInfo) { - writtenFrontmatter = {}; - return { - app: { - vault: { - getAbstractFileByPath: jest.fn().mockReturnValue(new TFile(task.path)), - modify: jest.fn(), - read: jest.fn().mockResolvedValue(""), - }, - workspace: { getActiveFile: jest.fn() }, - metadataCache: { getCache: jest.fn() }, - fileManager: { - processFrontMatter: jest.fn().mockImplementation((_file: any, fn: any) => { - // Start with empty frontmatter like Obsidian does - const fm: Record = {}; - fn(fm); - Object.assign(writtenFrontmatter, fm); - return Promise.resolve(); - }), - }, - }, - settings: { - taskFolder: "tasks", - fieldMapping: {}, - defaultTaskStatus: "open", - taskTag: "#task", - storeTitleInFilename: false, - resetCheckboxesOnRecurrence: false, - }, - statusManager: { - isCompletedStatus: jest.fn((s: string) => s === "done"), - getCompletedStatuses: jest.fn(() => ["done"]), +function buildMockPlugin(task: TaskInfo) { + const writtenFrontmatter: Record = {}; + const plugin = { + app: { + vault: { + getAbstractFileByPath: jest.fn().mockReturnValue(new TFile(task.path)), + modify: jest.fn(), + read: jest.fn().mockResolvedValue(""), }, - // Return field names as-is — the test checks the mapped field name - fieldMapper: { toUserField: jest.fn((f: string) => f) }, - cacheManager: { - getTaskInfo: jest.fn().mockResolvedValue(task), - updateTaskInfoInCache: jest.fn(), - waitForFreshTaskData: jest.fn().mockResolvedValue(undefined), + workspace: { getActiveFile: jest.fn() }, + metadataCache: { getCache: jest.fn() }, + fileManager: { + processFrontMatter: jest.fn().mockImplementation((_file: any, fn: any) => { + const fm: Record = {}; + fn(fm); + Object.assign(writtenFrontmatter, fm); + return Promise.resolve(); + }), }, - emitter: { trigger: jest.fn() }, - } as any; - } + }, + settings: { + taskFolder: "tasks", + fieldMapping: {}, + defaultTaskStatus: "open", + taskTag: "#task", + storeTitleInFilename: false, + resetCheckboxesOnRecurrence: false, + maintainDueDateOffsetInRecurring: false, + }, + statusManager: { + isCompletedStatus: jest.fn((s: string) => s === "done"), + getCompletedStatuses: jest.fn(() => ["done"]), + }, + fieldMapper: { toUserField: jest.fn((f: string) => f) }, + cacheManager: { + getTaskInfo: jest.fn().mockResolvedValue(task), + updateTaskInfoInCache: jest.fn(), + waitForFreshTaskData: jest.fn().mockResolvedValue(undefined), + }, + emitter: { trigger: jest.fn() }, + } as any; + return { plugin, writtenFrontmatter }; +} +describe("Issue #396 — recurring late completion records wrong date", () => { it("scheduled-anchor task completed late records the scheduled date, not today", async () => { - // Saturday task, completed on Sunday mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday - const saturdayTask = TaskFactory.createTask({ + const task = TaskFactory.createTask({ path: "tasks/test.md", title: "Weekly task", recurrence: "FREQ=WEEKLY;BYDAY=SA", @@ -84,13 +82,12 @@ describe("Issue #396 — recurring late completion records wrong date", () => { complete_instances: [], }); - const plugin = buildMockPlugin(saturdayTask); - taskService = new TaskService(plugin); + const { plugin, writtenFrontmatter } = buildMockPlugin(task); + const taskService = new TaskService(plugin); // Call WITHOUT explicit date — should default to scheduled, not today - await taskService.toggleRecurringTaskComplete(saturdayTask); + await taskService.toggleRecurringTaskComplete(task); - // completeInstances is the mapped field name (toUserField returns as-is) expect(writtenFrontmatter.completeInstances).toContain("2026-04-04"); expect(writtenFrontmatter.completeInstances).not.toContain("2026-04-05"); }); @@ -98,7 +95,7 @@ describe("Issue #396 — recurring late completion records wrong date", () => { it("completion-anchor task completed late records today, not the scheduled date", async () => { mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday - const completionTask = TaskFactory.createTask({ + const task = TaskFactory.createTask({ path: "tasks/test.md", title: "Completion-anchor task", recurrence: "FREQ=WEEKLY;BYDAY=SA", @@ -107,12 +104,11 @@ describe("Issue #396 — recurring late completion records wrong date", () => { complete_instances: [], }); - const plugin = buildMockPlugin(completionTask); - taskService = new TaskService(plugin); + const { plugin, writtenFrontmatter } = buildMockPlugin(task); + const taskService = new TaskService(plugin); - await taskService.toggleRecurringTaskComplete(completionTask); + await taskService.toggleRecurringTaskComplete(task); - // completion-anchor should use today (Sunday), not scheduled (Saturday) expect(writtenFrontmatter.completeInstances).toContain("2026-04-05"); expect(writtenFrontmatter.completeInstances).not.toContain("2026-04-04"); }); @@ -120,22 +116,89 @@ describe("Issue #396 — recurring late completion records wrong date", () => { it("undefined recurrence_anchor defaults to using the scheduled date", async () => { mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday - const defaultTask = TaskFactory.createTask({ + const task = TaskFactory.createTask({ path: "tasks/test.md", title: "Default anchor task", recurrence: "FREQ=WEEKLY;BYDAY=SA", scheduled: "2026-04-04", // Saturday complete_instances: [], }); - // Ensure recurrence_anchor is undefined (default = scheduled) - delete (defaultTask as any).recurrence_anchor; + delete (task as any).recurrence_anchor; - const plugin = buildMockPlugin(defaultTask); - taskService = new TaskService(plugin); + const { plugin, writtenFrontmatter } = buildMockPlugin(task); + const taskService = new TaskService(plugin); - await taskService.toggleRecurringTaskComplete(defaultTask); + await taskService.toggleRecurringTaskComplete(task); expect(writtenFrontmatter.completeInstances).toContain("2026-04-04"); expect(writtenFrontmatter.completeInstances).not.toContain("2026-04-05"); }); }); + +describe("Issue #396 — recurring late skip records wrong date", () => { + it("scheduled-anchor task skipped late records the scheduled date, not today", async () => { + mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday + + const task = TaskFactory.createTask({ + path: "tasks/test.md", + title: "Weekly task", + recurrence: "FREQ=WEEKLY;BYDAY=SA", + recurrence_anchor: "scheduled", + scheduled: "2026-04-04", // Saturday + complete_instances: [], + skipped_instances: [], + }); + + const { plugin, writtenFrontmatter } = buildMockPlugin(task); + const taskService = new TaskService(plugin); + + await taskService.toggleRecurringTaskSkipped(task); + + expect(writtenFrontmatter.skippedInstances).toContain("2026-04-04"); + expect(writtenFrontmatter.skippedInstances).not.toContain("2026-04-05"); + }); + + it("completion-anchor task skipped late records today, not the scheduled date", async () => { + mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday + + const task = TaskFactory.createTask({ + path: "tasks/test.md", + title: "Completion-anchor task", + recurrence: "FREQ=WEEKLY;BYDAY=SA", + recurrence_anchor: "completion", + scheduled: "2026-04-04", // Saturday + complete_instances: [], + skipped_instances: [], + }); + + const { plugin, writtenFrontmatter } = buildMockPlugin(task); + const taskService = new TaskService(plugin); + + await taskService.toggleRecurringTaskSkipped(task); + + expect(writtenFrontmatter.skippedInstances).toContain("2026-04-05"); + expect(writtenFrontmatter.skippedInstances).not.toContain("2026-04-04"); + }); + + it("undefined recurrence_anchor defaults to using the scheduled date for skip", async () => { + mockGetTodayLocal.mockReturnValue(new Date("2026-04-05T12:00:00")); // Sunday + + const task = TaskFactory.createTask({ + path: "tasks/test.md", + title: "Default anchor task", + recurrence: "FREQ=WEEKLY;BYDAY=SA", + scheduled: "2026-04-04", // Saturday + complete_instances: [], + skipped_instances: [], + }); + delete (task as any).recurrence_anchor; + + const { plugin, writtenFrontmatter } = buildMockPlugin(task); + const taskService = new TaskService(plugin); + + await taskService.toggleRecurringTaskSkipped(task); + + expect(writtenFrontmatter.skippedInstances).toContain("2026-04-04"); + expect(writtenFrontmatter.skippedInstances).not.toContain("2026-04-05"); + }); +}); From dc4744a712c7f504c9d020a5da0151e93d9acf39 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 5 Apr 2026 17:58:21 +0100 Subject: [PATCH 07/29] Fix auto-archive Google Calendar cleanup retries --- src/services/AutoArchiveService.ts | 48 ++++- ...oogle-calendar-archive-reliability.test.ts | 191 +++++++++++++++++- 2 files changed, 227 insertions(+), 12 deletions(-) diff --git a/src/services/AutoArchiveService.ts b/src/services/AutoArchiveService.ts index a43ee087f..d1918bbcc 100644 --- a/src/services/AutoArchiveService.ts +++ b/src/services/AutoArchiveService.ts @@ -19,6 +19,20 @@ export class AutoArchiveService { return !!task.googleCalendarEventId; } + private getCalendarCleanupState(): "ready" | "retry" | "skip" { + const googleCalendarExport = this.plugin.settings.googleCalendarExport; + + if (!googleCalendarExport?.enabled || !googleCalendarExport?.syncOnTaskDelete) { + return "skip"; + } + + if (!this.plugin.taskCalendarSyncService) { + return "retry"; + } + + return this.plugin.taskCalendarSyncService.isEnabled() ? "ready" : "retry"; + } + /** * Start the auto-archive service and begin periodic processing */ @@ -150,10 +164,19 @@ export class AutoArchiveService { } if (currentTask.archived) { - if ( - this.plugin.taskCalendarSyncService?.isEnabled() && - this.hasGoogleCalendarLink(currentTask) - ) { + if (this.hasGoogleCalendarLink(currentTask)) { + const calendarCleanupState = this.getCalendarCleanupState(); + if (calendarCleanupState === "skip") { + return true; + } + + if (calendarCleanupState === "retry") { + console.warn( + `Auto-archive Google cleanup deferred until calendar sync is ready for ${item.taskPath}` + ); + return false; + } + const deleted = await this.plugin.taskCalendarSyncService.deleteTaskFromCalendar(currentTask); if (!deleted) { @@ -171,11 +194,18 @@ export class AutoArchiveService { // Archive the task try { const archivedTask = await this.plugin.taskService.toggleArchive(currentTask); - if ( - archivedTask.archived && - this.plugin.taskCalendarSyncService?.isEnabled() && - this.hasGoogleCalendarLink(archivedTask) - ) { + if (archivedTask.archived && this.hasGoogleCalendarLink(archivedTask)) { + item.taskPath = archivedTask.path; + const calendarCleanupState = this.getCalendarCleanupState(); + if (calendarCleanupState === "skip") { + return true; + } + + if (calendarCleanupState === "retry") { + console.warn( + `Auto-archive Google cleanup deferred until calendar sync is ready for ${item.taskPath}` + ); + } return false; } return true; diff --git a/tests/unit/issues/issue-google-calendar-archive-reliability.test.ts b/tests/unit/issues/issue-google-calendar-archive-reliability.test.ts index dcbf0a5c7..96463d92f 100644 --- a/tests/unit/issues/issue-google-calendar-archive-reliability.test.ts +++ b/tests/unit/issues/issue-google-calendar-archive-reliability.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, jest, beforeEach } from "@jest/globals"; +import { describe, it, expect, jest } from "@jest/globals"; import { TFile } from "obsidian"; import { AutoArchiveService } from "../../../src/services/AutoArchiveService"; @@ -17,6 +17,17 @@ jest.mock("obsidian", () => ({ }, })); +const createGoogleCleanupEnabledPlugin = () => + PluginFactory.createMockPlugin({ + settings: { + ...PluginFactory.createMockPlugin().settings, + googleCalendarExport: { + enabled: true, + syncOnTaskDelete: true, + }, + }, + }); + describe("Google Calendar archive reliability", () => { it("preserves the Google Calendar event ID when deletion fails so cleanup can be retried", async () => { const frontmatter: Record = {}; @@ -97,7 +108,7 @@ describe("Google Calendar archive reliability", () => { }); it("keeps an auto-archive queue item pending when Google cleanup is still incomplete after archiving", async () => { - const plugin = PluginFactory.createMockPlugin(); + const plugin = createGoogleCleanupEnabledPlugin(); plugin.cacheManager.getTaskByPath = jest.fn(); plugin.taskService.toggleArchive = jest.fn(); plugin.taskCalendarSyncService = { @@ -136,7 +147,7 @@ describe("Google Calendar archive reliability", () => { }); it("retries Google cleanup for archived tasks that still have calendar links", async () => { - const plugin = PluginFactory.createMockPlugin(); + const plugin = createGoogleCleanupEnabledPlugin(); plugin.cacheManager.getTaskByPath = jest.fn(); plugin.taskService.toggleArchive = jest.fn(); plugin.taskCalendarSyncService = { @@ -168,4 +179,178 @@ describe("Google Calendar archive reliability", () => { ); expect(plugin.taskService.toggleArchive).not.toHaveBeenCalled(); }); + + it("persists the archived path in the retry queue when cleanup remains pending after an archive-folder move", async () => { + const plugin = createGoogleCleanupEnabledPlugin(); + const initialItem = { + taskPath: "TaskNotes/Tasks/archive-me.md", + statusChangeTimestamp: 0, + archiveAfterTimestamp: 0, + statusValue: "done", + }; + const pluginData = { autoArchiveQueue: [initialItem] }; + plugin.loadData = jest.fn().mockResolvedValue(pluginData); + plugin.saveData = jest.fn().mockResolvedValue(undefined); + plugin.cacheManager.getTaskByPath = jest.fn(); + plugin.taskService.toggleArchive = jest.fn(); + plugin.taskCalendarSyncService = { + isEnabled: jest.fn().mockReturnValue(true), + deleteTaskFromCalendar: jest.fn().mockResolvedValue(true), + }; + + const autoArchiveService = new AutoArchiveService(plugin); + const currentTask: TaskInfo = TaskFactory.createTask({ + path: "TaskNotes/Tasks/archive-me.md", + status: "done", + archived: false, + googleCalendarEventId: "master-event-id", + }); + const archivedTask: TaskInfo = { + ...currentTask, + path: "TaskNotes/Archive/archive-me.md", + archived: true, + tags: ["task", "archived"], + }; + + plugin.cacheManager.getTaskByPath.mockResolvedValue(currentTask); + plugin.taskService.toggleArchive.mockResolvedValue(archivedTask); + + await (autoArchiveService as any).processQueue(); + + expect(plugin.saveData).toHaveBeenCalledWith({ + autoArchiveQueue: [{ ...initialItem, taskPath: archivedTask.path }], + }); + }); + + it("keeps archived tasks in the retry queue until calendar sync is ready", async () => { + const plugin = createGoogleCleanupEnabledPlugin(); + const archivedItem = { + taskPath: "TaskNotes/Archive/archive-me.md", + statusChangeTimestamp: 0, + archiveAfterTimestamp: 0, + statusValue: "done", + }; + const pluginData = { autoArchiveQueue: [archivedItem] }; + plugin.loadData = jest.fn().mockResolvedValue(pluginData); + plugin.saveData = jest.fn().mockResolvedValue(undefined); + plugin.cacheManager.getTaskByPath = jest.fn(); + plugin.taskService.toggleArchive = jest.fn(); + plugin.taskCalendarSyncService = undefined; + + const autoArchiveService = new AutoArchiveService(plugin); + const archivedTask: TaskInfo = TaskFactory.createTask({ + path: archivedItem.taskPath, + status: "done", + archived: true, + tags: ["task", "archived"], + googleCalendarEventId: "master-event-id", + }); + + plugin.cacheManager.getTaskByPath.mockResolvedValue(archivedTask); + + await (autoArchiveService as any).processQueue(); + + expect(plugin.saveData).toHaveBeenCalledWith({ + autoArchiveQueue: [archivedItem], + }); + expect(plugin.taskService.toggleArchive).not.toHaveBeenCalled(); + }); + + it("drops archived tasks from the retry queue when Google cleanup is intentionally disabled", async () => { + const plugin = PluginFactory.createMockPlugin({ + settings: { + ...createGoogleCleanupEnabledPlugin().settings, + googleCalendarExport: { + enabled: false, + syncOnTaskDelete: true, + }, + }, + }); + const archivedItem = { + taskPath: "TaskNotes/Archive/archive-me.md", + statusChangeTimestamp: 0, + archiveAfterTimestamp: 0, + statusValue: "done", + }; + const pluginData = { autoArchiveQueue: [archivedItem] }; + plugin.loadData = jest.fn().mockResolvedValue(pluginData); + plugin.saveData = jest.fn().mockResolvedValue(undefined); + plugin.cacheManager.getTaskByPath = jest.fn(); + plugin.taskService.toggleArchive = jest.fn(); + plugin.taskCalendarSyncService = { + isEnabled: jest.fn().mockReturnValue(false), + deleteTaskFromCalendar: jest.fn().mockResolvedValue(true), + }; + + const autoArchiveService = new AutoArchiveService(plugin); + const archivedTask: TaskInfo = TaskFactory.createTask({ + path: archivedItem.taskPath, + status: "done", + archived: true, + tags: ["task", "archived"], + googleCalendarEventId: "master-event-id", + }); + + plugin.cacheManager.getTaskByPath.mockResolvedValue(archivedTask); + + await (autoArchiveService as any).processQueue(); + + expect(plugin.saveData).toHaveBeenCalledWith({ + autoArchiveQueue: [], + }); + expect(plugin.taskCalendarSyncService.deleteTaskFromCalendar).not.toHaveBeenCalled(); + expect(plugin.taskService.toggleArchive).not.toHaveBeenCalled(); + }); + + it("drops newly archived tasks from the retry queue when Google cleanup is intentionally disabled", async () => { + const plugin = PluginFactory.createMockPlugin({ + settings: { + ...createGoogleCleanupEnabledPlugin().settings, + googleCalendarExport: { + enabled: false, + syncOnTaskDelete: true, + }, + }, + }); + const initialItem = { + taskPath: "TaskNotes/Tasks/archive-me.md", + statusChangeTimestamp: 0, + archiveAfterTimestamp: 0, + statusValue: "done", + }; + const pluginData = { autoArchiveQueue: [initialItem] }; + plugin.loadData = jest.fn().mockResolvedValue(pluginData); + plugin.saveData = jest.fn().mockResolvedValue(undefined); + plugin.cacheManager.getTaskByPath = jest.fn(); + plugin.taskService.toggleArchive = jest.fn(); + plugin.taskCalendarSyncService = { + isEnabled: jest.fn().mockReturnValue(false), + deleteTaskFromCalendar: jest.fn().mockResolvedValue(true), + }; + + const autoArchiveService = new AutoArchiveService(plugin); + const currentTask: TaskInfo = TaskFactory.createTask({ + path: initialItem.taskPath, + status: "done", + archived: false, + googleCalendarEventId: "master-event-id", + }); + const archivedTask: TaskInfo = { + ...currentTask, + path: "TaskNotes/Archive/archive-me.md", + archived: true, + tags: ["task", "archived"], + }; + + plugin.cacheManager.getTaskByPath.mockResolvedValue(currentTask); + plugin.taskService.toggleArchive.mockResolvedValue(archivedTask); + + await (autoArchiveService as any).processQueue(); + + expect(plugin.saveData).toHaveBeenCalledWith({ + autoArchiveQueue: [], + }); + expect(plugin.taskService.toggleArchive).toHaveBeenCalledWith(currentTask); + expect(plugin.taskCalendarSyncService.deleteTaskFromCalendar).not.toHaveBeenCalled(); + }); }); From 447f04a278dbcc96a2792c0faf89ae27cc140a28 Mon Sep 17 00:00:00 2001 From: Paul Schmidt Date: Tue, 7 Apr 2026 23:23:34 +0200 Subject: [PATCH 08/29] feat: add option for input focus switching with Tab key Make the current Tab key behavior of focusing the next input field in the details part of the task modal toggleable. When this option is disabled, pressing Tab triggers the default behavior (e.g. indenting checkbox items) --- i18n.manifest.json | 2 + i18n.state.json | 64 ++++++++++++++++++++++++++++++++ src/i18n/resources/de.ts | 4 ++ src/i18n/resources/en.ts | 4 ++ src/i18n/resources/es.ts | 4 ++ src/i18n/resources/fr.ts | 4 ++ src/i18n/resources/ja.ts | 4 ++ src/i18n/resources/ko.ts | 4 ++ src/i18n/resources/pt.ts | 4 ++ src/i18n/resources/ru.ts | 4 ++ src/i18n/resources/zh.ts | 4 ++ src/modals/TaskModal.ts | 10 ++--- src/settings/defaults.ts | 1 + src/settings/tabs/featuresTab.ts | 13 +++++++ src/types/settings.ts | 1 + 15 files changed, 122 insertions(+), 5 deletions(-) diff --git a/i18n.manifest.json b/i18n.manifest.json index 3cdcbc94d..be5661ebb 100644 --- a/i18n.manifest.json +++ b/i18n.manifest.json @@ -322,6 +322,8 @@ "settings.features.instantConvert.toggle.description": "eed5789a053bf142162f60467c1fa80c1ed7830c", "settings.features.instantConvert.folder.name": "ee5974b000e8f6a84b0ad1dae3a0b0f106b5e7b2", "settings.features.instantConvert.folder.description": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", + "settings.features.switchFocusOnTab.name": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "settings.features.switchFocusOnTab.description": "a14785bd58840d00deeda04998e5ff0df6543cac", "settings.features.nlp.header": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "settings.features.nlp.description": "632a9af312f20952951f9f58447b064ffa833a08", "settings.features.nlp.enable.name": "e9b0bfb15d333ee3d894a349ed2a9f9e297be154", diff --git a/i18n.state.json b/i18n.state.json index 57d8f06f1..70fff83b8 100644 --- a/i18n.state.json +++ b/i18n.state.json @@ -1292,6 +1292,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "17486005628baac00ec8c047ea023cb1c42fce50" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "d4898917a4e5398fbb049772a34ed36bfa50ddf1" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "617c809a80a2bf9bd040b9010175474ea3f4a2c8" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "306d0086d726996ee9e2e6f0f9060f7b31dcbbd1" @@ -9562,6 +9570,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "1fb23ba9dabd5414106abea7f0e36b1e845102a5" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "a14785bd58840d00deeda04998e5ff0df6543cac" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "b78dcc6aa72d5dfc8097298b453d3c38725676d3" @@ -17832,6 +17848,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "7b8143d67c72876fb9930fd00b005d09381da471" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "bfcf2376999f1a3e14170c55f02adc8654c7b3d0" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "84a4350357e4181bde8c8e82653da351fee5a445" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "b1e8011b64c3e540c76e7200f83e8d4882c3f2b2" @@ -26102,6 +26126,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "54963b20aa549d3d17d76b9f7bf70f683ae0017a" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "ad1756dc9d71b6ca475c2de9f95e9db424a35532" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "6c2061f8087c23186344b858f78e49805da179c8" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "fc05d42078cafcbbe8df56b1752ab0a541f38dd2" @@ -34372,6 +34404,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "8fc3e9f020fa4481cdf1c7114b8091c2ffd64bdc" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "e64d41a76e45c6935009a3ad15b4351ba89bf83c" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "7578770d53a5253f6693f0738f49883a22e6150e" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "4b046cbf82ae9d360a12fd3c7631834919812a55" @@ -42642,6 +42682,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "74a241bba4f841528a71eee9cdadcf31086c21c2" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "69a2ee93469ee562dd557c698e091e46c1f7ceb2" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "6d4ebba516f2d9cc99ac105a5e20219eab8fad74" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "bcaadfba464e1d20c30c42a9c52e5c711c40366c" @@ -50912,6 +50960,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "79a9fdc5ec3efd53d942c87a77d1024d8bf00326" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "aa7ab02810e0ea2c6ede0462c3983a462cc0769b" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "2cf586d43dd2271e986f53bbf54c18037a38baec" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "2c59d2dd46678464aad80724320867faf0bee86d" @@ -59182,6 +59238,14 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "ec72bd6d2d63718c6c9ccfcf1e88e4ce93402840" }, + "settings.features.switchFocusOnTab.name": { + "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", + "translation": "3ff9f0a8532808d51a9398827db789b0b918db05" + }, + "settings.features.switchFocusOnTab.description": { + "source": "a14785bd58840d00deeda04998e5ff0df6543cac", + "translation": "a2843c8634654664d021938f01df7d7a6c6212aa" + }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "84a852be5572401ef59a98f04c21d40008aac4a4" diff --git a/src/i18n/resources/de.ts b/src/i18n/resources/de.ts index 7fe96a641..64ff07beb 100644 --- a/src/i18n/resources/de.ts +++ b/src/i18n/resources/de.ts @@ -485,6 +485,10 @@ export const de: TranslationTree = { description: "Ordner, in dem aus Checkboxen konvertierte Aufgaben erstellt werden. Leer lassen, um den Standard-Aufgabenordner zu verwenden. Verwende {{currentNotePath}} für den Ordner der aktuellen Notiz oder {{currentNoteTitle}} für einen Unterordner mit dem Notiztitel.", }, }, + switchFocusOnTab: { + name: "Fokus mit Tabulatortaste wechseln", + description: "Wechsle beim Bearbeiten von Aufgabendetails mit der Tabulatortaste den Eingabefokus", + }, nlp: { header: "Natürliche Sprachverarbeitung", description: "Analysiere Daten, Prioritäten und andere Eigenschaften aus Texteingaben.", diff --git a/src/i18n/resources/en.ts b/src/i18n/resources/en.ts index e31b657b7..f76191347 100644 --- a/src/i18n/resources/en.ts +++ b/src/i18n/resources/en.ts @@ -486,6 +486,10 @@ export const en: TranslationTree = { "Folder where tasks converted from checkboxes will be created. Leave empty to use the default tasks folder. Use {{currentNotePath}} for the current note's folder, or {{currentNoteTitle}} for a subfolder named after the current note.", }, }, + switchFocusOnTab: { + name: "Cambia el foco con la tecla Tab", + description: "Al editar los detalles de una tarea, cambia el foco de entrada con la tecla Tab", + }, nlp: { header: "Natural Language Processing", description: "Parse dates, priorities, and other properties from text input.", diff --git a/src/i18n/resources/es.ts b/src/i18n/resources/es.ts index 9752edd04..2a04c6010 100644 --- a/src/i18n/resources/es.ts +++ b/src/i18n/resources/es.ts @@ -485,6 +485,10 @@ export const es: TranslationTree = { description: "Carpeta donde se crearán las tareas convertidas desde casillas de verificación. Dejar vacío para usar la carpeta de tareas predeterminada. Usa {{currentNotePath}} para la carpeta de la nota actual, o {{currentNoteTitle}} para una subcarpeta con el título de la nota.", }, }, + switchFocusOnTab: { + name: "Cambia el foco con la tecla Tab", + description: "Al editar los detalles de una tarea, cambia el foco de entrada con la tecla Tab", + }, nlp: { header: "Procesamiento de lenguaje natural", description: "Analiza fechas, prioridades y otras propiedades desde texto de entrada.", diff --git a/src/i18n/resources/fr.ts b/src/i18n/resources/fr.ts index 5aff89edd..f4efa9f60 100644 --- a/src/i18n/resources/fr.ts +++ b/src/i18n/resources/fr.ts @@ -485,6 +485,10 @@ export const fr: TranslationTree = { description: "Dossier où les tâches converties depuis les cases à cocher seront créées. Laisser vide pour utiliser le dossier de tâches par défaut. Utilisez {{currentNotePath}} pour le dossier de la note actuelle, ou {{currentNoteTitle}} pour un sous-dossier nommé d'après la note.", }, }, + switchFocusOnTab: { + name: "Déplacer le curseur à l'aide de la touche Tab", + description: "Lors de la modification des détails d'une tâche, utilisez la touche Tab pour déplacer le curseur", + }, nlp: { header: "Traitement du langage naturel", description: "Analyse les dates, priorités et autres propriétés depuis le texte saisi.", diff --git a/src/i18n/resources/ja.ts b/src/i18n/resources/ja.ts index 5aeb6722f..3cd27f464 100644 --- a/src/i18n/resources/ja.ts +++ b/src/i18n/resources/ja.ts @@ -485,6 +485,10 @@ export const ja: TranslationTree = { description: "チェックボックスから変換されたタスクが作成されるフォルダー。空白のままにするとデフォルトのタスクフォルダーが使用されます。{{currentNotePath}}で現在のノートのフォルダー、{{currentNoteTitle}}でノートのタイトルを持つサブフォルダーを指定できます。", }, }, + switchFocusOnTab: { + name: "Tabキーでフォーカスを移動します", + description: "タスクの詳細を編集する際は、Tabキーで入力フォーカスを移動してください", + }, nlp: { header: "自然言語処理", description: "テキスト入力から日付、優先度、その他のプロパティを解析します。", diff --git a/src/i18n/resources/ko.ts b/src/i18n/resources/ko.ts index 68d50fbd1..f4fd86a4a 100644 --- a/src/i18n/resources/ko.ts +++ b/src/i18n/resources/ko.ts @@ -481,6 +481,10 @@ export const ko: TranslationTree = { description: "체크박스에서 변환된 작업이 생성될 폴더. 기본 작업 폴더를 사용하려면 비워두세요. {{currentNotePath}}는 현재 노트 폴더, {{currentNoteTitle}}은 현재 노트 이름의 하위 폴더입니다.", }, }, + switchFocusOnTab: { + name: "탭 키를 사용하여 포커스를 이동합니다", + description: "작업 세부 정보를 편집할 때 탭 키를 사용하여 입력 포커스를 이동합니다", + }, nlp: { header: "자연어 처리", description: "텍스트 입력에서 날짜, 우선순위 및 기타 속성을 파싱합니다.", diff --git a/src/i18n/resources/pt.ts b/src/i18n/resources/pt.ts index bfefc6676..514d6bb7b 100644 --- a/src/i18n/resources/pt.ts +++ b/src/i18n/resources/pt.ts @@ -485,6 +485,10 @@ export const pt: TranslationTree = { description: "Pasta onde tarefas convertidas de caixas de seleção serão criadas. Deixe vazio para usar a pasta de tarefas padrão. Use {{currentNotePath}} para a pasta da nota atual, ou {{currentNoteTitle}} para uma subpasta com o título da nota." } }, + switchFocusOnTab: { + name: "Alterar o foco com a tecla Tab", + description: "Ao editar os detalhes de uma tarefa, utilize a tecla Tab para alterar o foco de entrada", + }, nlp: { header: "Processamento de Linguagem Natural", description: "Analisa datas, prioridades e outras propriedades do texto inserido.", diff --git a/src/i18n/resources/ru.ts b/src/i18n/resources/ru.ts index d14995dca..fe16e9bf4 100644 --- a/src/i18n/resources/ru.ts +++ b/src/i18n/resources/ru.ts @@ -485,6 +485,10 @@ export const ru: TranslationTree = { description: "Папка, в которой будут создаваться задачи, преобразованные из флажков. Оставьте пустым для использования папки задач по умолчанию. Используйте {{currentNotePath}} для папки текущей заметки или {{currentNoteTitle}} для подпапки с названием заметки.", }, }, + switchFocusOnTab: { + name: "Перемещайте фокус с помощью клавиши Tab", + description: "При редактировании сведений о задании перемещайте фокус ввода с помощью клавиши Tab", + }, nlp: { header: "Обработка естественного языка", description: "Анализ дат, приоритетов и других свойств из текстового ввода.", diff --git a/src/i18n/resources/zh.ts b/src/i18n/resources/zh.ts index 38ecb86e3..1959e319a 100644 --- a/src/i18n/resources/zh.ts +++ b/src/i18n/resources/zh.ts @@ -485,6 +485,10 @@ export const zh: TranslationTree = { description: "从复选框转换的任务将在其中创建的文件夹。留空则使用默认任务文件夹。使用{{currentNotePath}}表示当前笔记的文件夹,或使用{{currentNoteTitle}}表示以笔记标题命名的子文件夹。", }, }, + switchFocusOnTab: { + name: "使用 Tab 键切换焦点", + description: "在编辑任务详情时,使用 Tab 键切换输入焦点", + }, nlp: { header: "自然语言处理", description: "从文本输入解析日期、优先级和其他属性。", diff --git a/src/modals/TaskModal.ts b/src/modals/TaskModal.ts index cd9018091..2b15fb2f2 100644 --- a/src/modals/TaskModal.ts +++ b/src/modals/TaskModal.ts @@ -789,6 +789,10 @@ export abstract class TaskModal extends Modal { // Create container for the markdown editor const detailsEditorContainer = rightColumn.createDiv("details-markdown-editor"); + const onTabCallback = this.plugin.settings.switchFocusOnTab + ? () => {this.focusNextField(); return true;} // jump to next input field & prevent default tab behavior + : () => false; // default behavior + // Create embeddable markdown editor for details using shared method this.detailsMarkdownEditor = createTaskModalMarkdownEditor(this.app, detailsEditorContainer, { value: this.details, @@ -805,11 +809,7 @@ export abstract class TaskModal extends Modal { // ESC - close the modal this.close(); }, - onTab: () => { - // Tab - jump to next input field - this.focusNextField(); - return true; // Prevent default tab behavior - }, + onTab: onTabCallback, }); } diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 169ec7f8a..9ea628f0c 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -295,6 +295,7 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = { disableOverlayOnAlias: false, enableInstantTaskConvert: true, useDefaultsOnInstantConvert: true, + switchFocusOnTab: true, enableNaturalLanguageInput: true, nlpDefaultToScheduled: true, nlpLanguage: "en", // Default to English diff --git a/src/settings/tabs/featuresTab.ts b/src/settings/tabs/featuresTab.ts index e79c2cea8..d691f4e5b 100644 --- a/src/settings/tabs/featuresTab.ts +++ b/src/settings/tabs/featuresTab.ts @@ -108,6 +108,19 @@ export function renderFeaturesTab( }, }) ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.switchFocusOnTab.name"), + desc: translate("settings.features.switchFocusOnTab.description"), + getValue: () => plugin.settings.switchFocusOnTab, + setValue: async (value: boolean) => { + plugin.settings.switchFocusOnTab = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); } ); diff --git a/src/types/settings.ts b/src/types/settings.ts index 75bc40155..3bc5da44e 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -127,6 +127,7 @@ export interface TaskNotesSettings { disableOverlayOnAlias: boolean; enableInstantTaskConvert: boolean; useDefaultsOnInstantConvert: boolean; + switchFocusOnTab: boolean; enableNaturalLanguageInput: boolean; nlpDefaultToScheduled: boolean; nlpLanguage: string; // Language code for natural language processing (e.g., 'en', 'es', 'fr') From 8066d10e4506f93bbced9abcf766fe3b02ca2393 Mon Sep 17 00:00:00 2001 From: Paul Schmidt Date: Tue, 7 Apr 2026 23:34:02 +0200 Subject: [PATCH 09/29] Replace wrong translation for switchFocusOnTab setting --- i18n.manifest.json | 4 ++-- src/i18n/resources/en.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n.manifest.json b/i18n.manifest.json index be5661ebb..672bd06be 100644 --- a/i18n.manifest.json +++ b/i18n.manifest.json @@ -322,8 +322,8 @@ "settings.features.instantConvert.toggle.description": "eed5789a053bf142162f60467c1fa80c1ed7830c", "settings.features.instantConvert.folder.name": "ee5974b000e8f6a84b0ad1dae3a0b0f106b5e7b2", "settings.features.instantConvert.folder.description": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", - "settings.features.switchFocusOnTab.name": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "settings.features.switchFocusOnTab.description": "a14785bd58840d00deeda04998e5ff0df6543cac", + "settings.features.switchFocusOnTab.name": "960fb4f9873c1de5b6845c82fb870fbbc6b73b2f", + "settings.features.switchFocusOnTab.description": "76fdb1ff4256e6bdca9bcac2857b5a0ad5c1743c", "settings.features.nlp.header": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "settings.features.nlp.description": "632a9af312f20952951f9f58447b064ffa833a08", "settings.features.nlp.enable.name": "e9b0bfb15d333ee3d894a349ed2a9f9e297be154", diff --git a/src/i18n/resources/en.ts b/src/i18n/resources/en.ts index f76191347..534bc8cdf 100644 --- a/src/i18n/resources/en.ts +++ b/src/i18n/resources/en.ts @@ -487,8 +487,8 @@ export const en: TranslationTree = { }, }, switchFocusOnTab: { - name: "Cambia el foco con la tecla Tab", - description: "Al editar los detalles de una tarea, cambia el foco de entrada con la tecla Tab", + name: "Switch focus on Tab", + description: "Switch input focus to the next field when presssing the Tab key while editing details of a task", }, nlp: { header: "Natural Language Processing", From 1b6b52f6a432ad49afe3bb8a505d9b485082dd01 Mon Sep 17 00:00:00 2001 From: Renato Mendonca Date: Sat, 11 Apr 2026 17:18:12 +1200 Subject: [PATCH 10/29] fix: restore subgroup indentation in TaskList views --- styles/task-list-view.css | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/styles/task-list-view.css b/styles/task-list-view.css index a6b4b7985..0efc8c08e 100644 --- a/styles/task-list-view.css +++ b/styles/task-list-view.css @@ -166,6 +166,28 @@ .tasknotes-plugin .task-subgroup.is-collapsed .task-cards { display: none; } .tasknotes-plugin .task-group.is-collapsed .task-subgroups-container { display: none; } +/* Sub-header indentation for data-level attribute */ +.tasknotes-plugin .task-group[data-level="sub"] { + padding-left: 16px; + margin-bottom: var(--tn-spacing-sm); +} + +.tasknotes-plugin .task-group[data-level="sub"] .task-group-header { + font-size: 0.95em; + font-weight: 500; + color: var(--tn-text-muted); + padding: var(--tn-spacing-xs) var(--tn-spacing-sm); +} + +.tasknotes-plugin .task-group[data-level="sub"] .task-group-toggle { + opacity: 0.8; +} + +.tasknotes-plugin .task-group[data-level="sub"] .task-group-toggle svg { + width: 14px; + height: 14px; +} + /* Per-group subgroup actions */ .tasknotes-plugin .task-subgroup-actions { display:flex; align-items:center; gap: 4px; margin-left: 0; } .tasknotes-plugin .task-subgroup-action { border:none; background:transparent; color: var(--tn-text-muted); cursor: var(--cursor, pointer); padding: 0; } From 390a1751a0c3cdb2b799a05603960039ab003214 Mon Sep 17 00:00:00 2001 From: Renato Mendonca Date: Sun, 12 Apr 2026 22:56:54 +1200 Subject: [PATCH 11/29] fix: align task card chevrons with subgroup header chevrons Subgroup header indentation CSS selectors had a typo (tn-tasknoteTaskList instead of tn-tasknotesTaskList) causing the rules to never match. Also added margin-left to task cards under subgroups so their chevrons align directly below the subgroup header chevron. --- styles/bases-views.css | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/styles/bases-views.css b/styles/bases-views.css index 3aa9a066e..05dda33a6 100644 --- a/styles/bases-views.css +++ b/styles/bases-views.css @@ -94,23 +94,23 @@ } /* Sub-group header styling */ -.tn-tasknoteTaskList .task-group[data-level="sub"] { +.tn-tasknotesTaskList .task-group[data-level="sub"] { padding-left: 16px; /* Subtle indentation */ margin-bottom: var(--tn-spacing-sm); } -.tn-tasknoteTaskList .task-group[data-level="sub"] .task-group-header { +.tn-tasknotesTaskList .task-group[data-level="sub"] .task-group-header { font-size: 0.95em; font-weight: 500; /* Less bold than primary */ color: var(--tn-text-muted); padding: var(--tn-spacing-xs) var(--tn-spacing-sm); } -.tn-tasknoteTaskList .task-group[data-level="sub"] .task-group-toggle { +.tn-tasknotesTaskList .task-group[data-level="sub"] .task-group-toggle { opacity: 0.8; /* Slightly more subtle chevron */ } -.tn-tasknoteTaskList .task-group[data-level="sub"] .task-group-toggle svg { +.tn-tasknotesTaskList .task-group[data-level="sub"] .task-group-toggle svg { width: 14px; height: 14px; } @@ -123,6 +123,17 @@ padding-left: var(--tn-spacing-md); } +/* Reset indentation for task cards after a primary group header */ +.tn-tasknotesTaskList .task-group[data-level="primary"] ~ .task-card { + margin-left: 0; +} + +/* Indent task cards under subgroups to align chevrons with subgroup header chevron. + This rule comes after the primary reset so it wins for cards following a subgroup. */ +.tn-tasknotesTaskList .task-group[data-level="sub"] ~ .task-card { + margin-left: 16px; +} + /* Empty state */ .tn-bases-empty { display: flex; From 2aa1a50d74e46db914b3b32bbc012ee5a5626e08 Mon Sep 17 00:00:00 2001 From: Sigma Date: Mon, 20 Apr 2026 03:39:13 +0900 Subject: [PATCH 12/29] Update Japanese translations for Kanban terms --- src/i18n/resources/ja.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/resources/ja.ts b/src/i18n/resources/ja.ts index 5aeb6722f..dd8eb2d51 100644 --- a/src/i18n/resources/ja.ts +++ b/src/i18n/resources/ja.ts @@ -282,7 +282,7 @@ export const ja: TranslationTree = { }, }, kanban: { - title: "かんばん", + title: "カンバン", newTask: "新しいタスク", addCard: "+ カードを追加", noTasks: "タスクなし", @@ -295,12 +295,12 @@ export const ja: TranslationTree = { }, }, notices: { - loadFailed: "かんばんボードの読み込みに失敗しました", + loadFailed: "カンバンボードの読み込みに失敗しました", movedTask: "タスクを\"{0}\"に移動しました", }, errors: { loadingBoard: "ボードの読み込みエラー。", - noGroupBy: "かんばんビューには「グループ化」プロパティの設定が必要です。「並び替え」ボタンをクリックし、「グループ化」でプロパティを選択してください。", + noGroupBy: "カンバンビューには「グループ化」プロパティの設定が必要です。「並び替え」ボタンをクリックし、「グループ化」でプロパティを選択してください。", formulaGroupingReadOnly: "数式ベースの列間でタスクを移動することはできません。数式の値は計算されるため、直接変更することはできません。", formulaSwimlaneReadOnly: "数式ベースのスイムレーン間でタスクを移動することはできません。数式の値は計算されるため、直接変更することはできません。", }, @@ -2119,7 +2119,7 @@ export const ja: TranslationTree = { openNotesView: "ノートビューを開く", openAgendaView: "アジェンダビューを開く", openPomodoroView: "ポモドーロタイマーを開く", - openKanbanView: "かんばんボードを開く", + openKanbanView: "カンバンボードを開く", openPomodoroStats: "ポモドーロ統計を開く", openStatisticsView: "タスクとプロジェクト統計を開く", createNewTask: "新しいタスクを作成", From ad07201b0746413c7a25f00908e2e3a85003c319 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 10:53:40 +1000 Subject: [PATCH 13/29] Polish recurring sync fixes --- docs/releases/unreleased.md | 5 +++ src/main.ts | 19 ++-------- src/services/TaskCalendarSyncService.ts | 6 ++-- src/services/TaskService.ts | 46 +++++++++++++------------ 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index aa1214690..39324fdb2 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -23,3 +23,8 @@ Example: ``` --> + +## Fixed + +- (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. + - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. diff --git a/src/main.ts b/src/main.ts index 1764bd11c..68a66326f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,7 @@ import { TaskEditModal } from "./modals/TaskEditModal"; import { openTaskSelector } from "./modals/TaskSelectorWithCreateModal"; import { PomodoroService } from "./services/PomodoroService"; import { formatTime, getActiveTimeEntry } from "./utils/helpers"; -import { convertUTCToLocalCalendarDate, getCurrentTimestamp, getDatePart, parseDateToUTC } from "./utils/dateUtils"; +import { convertUTCToLocalCalendarDate, getCurrentTimestamp } from "./utils/dateUtils"; import { TaskManager } from "./utils/TaskManager"; import { DependencyCache } from "./utils/DependencyCache"; import { RequestDeduplicator, PredictivePrefetcher } from "./utils/RequestDeduplicator"; @@ -49,7 +49,6 @@ import { ViewStateManager } from "./services/ViewStateManager"; import { DragDropManager } from "./utils/DragDropManager"; import { formatDateForStorage, - createUTCDateFromLocalCalendarDate, parseDateToLocal, getTodayLocal, } from "./utils/dateUtils"; @@ -1060,20 +1059,8 @@ export default class TaskNotesPlugin extends Plugin { */ async toggleRecurringTaskComplete(task: TaskInfo, date?: Date): Promise { try { - // Resolve the implicit date from cacheManager — the same authoritative - // source the service uses — before the service mutates state (#396) - const freshTask = (await this.cacheManager.getTaskInfo(task.path)) || task; - const targetDate = - date || - (() => { - if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { - return parseDateToUTC(getDatePart(freshTask.scheduled)); - } - const todayLocal = getTodayLocal(); - return createUTCDateFromLocalCalendarDate(todayLocal); - })(); - - const updatedTask = await this.taskService.toggleRecurringTaskComplete(task, date); + const targetDate = await this.taskService.resolveRecurringTaskActionDate(task, date); + const updatedTask = await this.taskService.toggleRecurringTaskComplete(task, targetDate); const dateStr = formatDateForStorage(targetDate); const wasCompleted = updatedTask.complete_instances?.includes(dateStr); diff --git a/src/services/TaskCalendarSyncService.ts b/src/services/TaskCalendarSyncService.ts index 439229d82..01af8cc3a 100644 --- a/src/services/TaskCalendarSyncService.ts +++ b/src/services/TaskCalendarSyncService.ts @@ -19,8 +19,6 @@ const GOOGLE_API_CALL_SPACING_MS = 100; /** * Service for syncing TaskNotes tasks to Google Calendar. * Handles creating, updating, and deleting calendar events when tasks change. - * - * Patched locally: __codexTaskCalendarDebounceStaleCachePatch20260404 */ export class TaskCalendarSyncService { private plugin: TaskNotesPlugin; @@ -789,9 +787,9 @@ export class TaskCalendarSyncService { const latestTask = this.pendingTasks.get(taskPath); this.pendingTasks.delete(taskPath); - // Fallback to cache ONLY if for some reason the pending task is missing + // Fallback to cache only if the pending task is missing const freshTask = latestTask || await this.plugin.cacheManager.getTaskInfo(taskPath); - + if (!freshTask) { resolve(); return; diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts index e66750d65..2a3b39b7f 100644 --- a/src/services/TaskService.ts +++ b/src/services/TaskService.ts @@ -1277,6 +1277,28 @@ export class TaskService { /** * Toggle completion status for recurring tasks on a specific date */ + async resolveRecurringTaskActionDate(task: TaskInfo, date?: Date): Promise { + if (date) { + return date; + } + + const freshTask = (await this.plugin.cacheManager.getTaskInfo(task.path)) || task; + return this.getRecurringTaskActionDate(freshTask); + } + + private getRecurringTaskActionDate(task: TaskInfo, date?: Date): Date { + if (date) { + return date; + } + + if (task.recurrence_anchor !== "completion" && task.scheduled) { + return parseDateToUTC(getDatePart(task.scheduled)); + } + + const todayLocal = getTodayLocal(); + return createUTCDateFromLocalCalendarDate(todayLocal); + } + async toggleRecurringTaskComplete(task: TaskInfo, date?: Date): Promise { const file = this.plugin.app.vault.getAbstractFileByPath(task.path); if (!(file instanceof TFile)) { @@ -1290,17 +1312,7 @@ export class TaskService { throw new Error("Task is not recurring"); } - // For scheduled-anchor recurring tasks, default to the scheduled occurrence - // date — not today — so late completions mark the correct instance (#396) - const targetDate = - date || - (() => { - if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { - return parseDateToUTC(getDatePart(freshTask.scheduled)); - } - const todayLocal = getTodayLocal(); - return createUTCDateFromLocalCalendarDate(todayLocal); - })(); + const targetDate = this.getRecurringTaskActionDate(freshTask, date); const dateStr = formatDateForStorage(targetDate); // Check current completion status for this date using fresh data @@ -1519,17 +1531,7 @@ export class TaskService { throw new Error("Task is not recurring"); } - // For scheduled-anchor recurring tasks, default to the scheduled occurrence - // date — not today — so late skips mark the correct instance (#396) - const targetDate = - date || - (() => { - if (freshTask.recurrence_anchor !== "completion" && freshTask.scheduled) { - return parseDateToUTC(getDatePart(freshTask.scheduled)); - } - const todayLocal = getTodayLocal(); - return createUTCDateFromLocalCalendarDate(todayLocal); - })(); + const targetDate = this.getRecurringTaskActionDate(freshTask, date); const dateStr = formatDateForStorage(targetDate); // Check current skip status for this date From d01b823e4c1387594c8911bcb736026991295f1c Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 11:46:44 +1000 Subject: [PATCH 14/29] Fix NLP package resolution in tests --- docs/releases/unreleased.md | 1 + jest.config.js | 1 - package-lock.json | 12 ++++++------ package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 39324fdb2..a4dcc0534 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -28,3 +28,4 @@ Example: - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. +- Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. diff --git a/jest.config.js b/jest.config.js index 59b4047f0..69001ab10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,7 +11,6 @@ module.exports = { }, setupFilesAfterEnv: ['/tests/test-setup.ts'], moduleNameMapper: { - '^tasknotes-nlp-core$': '/../tasknotes-nlp-core/src/index.ts', '^obsidian$': '/tests/__mocks__/obsidian.ts', '^@fullcalendar/(.*)$': '/tests/__mocks__/fullcalendar.ts', // Keep mocks for complex/large libraries that benefit from controlled testing diff --git a/package-lock.json b/package-lock.json index 5361ced62..5b7c08f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tasknotes", - "version": "4.4.0", + "version": "4.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tasknotes", - "version": "4.4.0", + "version": "4.5.1", "license": "MIT", "dependencies": { "@codemirror/view": "^6.37.2", @@ -24,7 +24,7 @@ "obsidian-daily-notes-interface": "^0.9.4", "reflect-metadata": "^0.2.2", "rrule": "^2.8.1", - "tasknotes-nlp-core": "^0.1.0", + "tasknotes-nlp-core": "^0.1.1", "yaml": "^2.3.1", "zod": "^3.24.0" }, @@ -13082,9 +13082,9 @@ } }, "node_modules/tasknotes-nlp-core": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tasknotes-nlp-core/-/tasknotes-nlp-core-0.1.0.tgz", - "integrity": "sha512-A8yw2D8VO9VD+h21c5rJ040VcIga+phUo5/XY49tFAfViahSYUNtZUPNCSZUci59DK9cFsZ0MB81arHJyQd5+Q==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tasknotes-nlp-core/-/tasknotes-nlp-core-0.1.1.tgz", + "integrity": "sha512-TRc+E6aZ09+Q3g2SQffnnsA7HZqu3aqJFAS5CXqOyiy1/P4RdgiiOyTnwCBNob1EuLo7UbNyHF1zsjdHRljJqQ==", "license": "MIT", "dependencies": { "chrono-node": "^2.7.5", diff --git a/package.json b/package.json index bfb2bd98d..4b76e804b 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "obsidian-daily-notes-interface": "^0.9.4", "reflect-metadata": "^0.2.2", "rrule": "^2.8.1", - "tasknotes-nlp-core": "^0.1.0", + "tasknotes-nlp-core": "^0.1.1", "yaml": "^2.3.1", "zod": "^3.24.0" } From 72d0a72f8c4dc8abc5f762d174089314ad5a8dc7 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Mon, 6 Apr 2026 10:52:54 +1000 Subject: [PATCH 15/29] fix 1744 --- .gitignore | 1 + docs/releases/unreleased.md | 3 +++ src/bases/KanbanView.ts | 27 ++++++++++++++++++++++++++- src/bases/TaskListView.ts | 32 +++++++++++++++++++++++++++++--- src/ui/TaskCard.ts | 32 +++++++++++++++++++++++++++++++- 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e5f167124..8ba6b6b32 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,4 @@ AGENTS.md /docs-builder/dist .ops/ .serena/ +/.codex diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index a4dcc0534..bcfcbdb78 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -29,3 +29,6 @@ Example: - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. - Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. +- (#1744) Fixed Bases Task List views so changing the per-view `Expanded relationships` option takes effect on re-render + - Restores the expected `show-all` behavior for setups that hide subtasks at the top level with `note.projects.isEmpty()` + - Thanks to @minol-dev for reporting diff --git a/src/bases/KanbanView.ts b/src/bases/KanbanView.ts index 91cba41de..43c82a466 100644 --- a/src/bases/KanbanView.ts +++ b/src/bases/KanbanView.ts @@ -23,6 +23,30 @@ import { DropOperationQueue, } from "./sortOrderUtils"; +function normalizeExpandedRelationshipFilterMode( + value: unknown +): "inherit" | "show-all" { + if (typeof value === "number") { + return value === 1 ? "show-all" : "inherit"; + } + + const normalized = String(value ?? "") + .trim() + .toLowerCase() + .replace(/^['"]|['"]$/g, "") + .replace(/[_\s]+/g, "-"); + + if (normalized === "show-all" || normalized === "1") { + return "show-all"; + } + + if (normalized === "inherit" || normalized === "0") { + return "inherit"; + } + + return "inherit"; +} + export class KanbanView extends BasesViewBase { type = "tasknotesKanban"; @@ -184,7 +208,7 @@ export class KanbanView extends BasesViewBase { "expandedRelationshipFilterMode" ); this.expandedRelationshipFilterMode = - expandedRelationshipFilterModeValue === "show-all" ? "show-all" : "inherit"; + normalizeExpandedRelationshipFilterMode(expandedRelationshipFilterModeValue); // Mark config as successfully loaded this.configLoaded = true; @@ -3062,6 +3086,7 @@ export class KanbanView extends BasesViewBase { targetDate, hideStatusIndicator, expandedRelationshipFilterMode: this.expandedRelationshipFilterMode, + resolveExpandedRelationshipFilterMode: () => this.config?.get("expandedRelationshipFilterMode"), expandedRelationshipTaskPaths: this.currentVisibleTaskPaths, }); } diff --git a/src/bases/TaskListView.ts b/src/bases/TaskListView.ts index 7118a06db..029dae7e7 100644 --- a/src/bases/TaskListView.ts +++ b/src/bases/TaskListView.ts @@ -44,6 +44,30 @@ type TaskListInsertionSlot = { position: "before" | "after"; }; +function normalizeExpandedRelationshipFilterMode( + value: unknown +): "inherit" | "show-all" { + if (typeof value === "number") { + return value === 1 ? "show-all" : "inherit"; + } + + const normalized = String(value ?? "") + .trim() + .toLowerCase() + .replace(/^['"]|['"]$/g, "") + .replace(/[_\s]+/g, "-"); + + if (normalized === "show-all" || normalized === "1") { + return "show-all"; + } + + if (normalized === "inherit" || normalized === "0") { + return "inherit"; + } + + return "inherit"; +} + export class TaskListView extends BasesViewBase { type = "tasknotesTaskList"; @@ -135,7 +159,7 @@ export class TaskListView extends BasesViewBase { "expandedRelationshipFilterMode" ); this.expandedRelationshipFilterMode = - expandedRelationshipFilterModeValue === "show-all" ? "show-all" : "inherit"; + normalizeExpandedRelationshipFilterMode(expandedRelationshipFilterModeValue); // Mark config as successfully loaded this.configLoaded = true; } catch (e) { @@ -179,8 +203,9 @@ export class TaskListView extends BasesViewBase { return; } - // Ensure view options are read (in case config wasn't available in onload) - if (!this.configLoaded && this.config) { + // Always re-read view options to catch config changes such as + // switching expanded relationship filtering modes in Bases. + if (this.config) { this.readViewOptions(); } @@ -1748,6 +1773,7 @@ export class TaskListView extends BasesViewBase { return this.buildTaskCardOptions({ targetDate, expandedRelationshipFilterMode: this.expandedRelationshipFilterMode, + resolveExpandedRelationshipFilterMode: () => this.config?.get("expandedRelationshipFilterMode"), expandedRelationshipTaskPaths: this.currentVisibleTaskPaths, }); } diff --git a/src/ui/TaskCard.ts b/src/ui/TaskCard.ts index 3a28a1c4f..35a3fb955 100644 --- a/src/ui/TaskCard.ts +++ b/src/ui/TaskCard.ts @@ -61,6 +61,8 @@ export interface TaskCardOptions { propertyLabels?: TaskCardPresentationOptions["propertyLabels"]; /** How expanded subtasks/dependencies should interact with the current view filter. */ expandedRelationshipFilterMode?: "inherit" | "show-all"; + /** Optional live resolver for the current expanded relationship filter mode. */ + resolveExpandedRelationshipFilterMode?: () => "inherit" | "show-all"; /** Paths visible in the current view after Bases/search filtering. */ expandedRelationshipTaskPaths?: ReadonlySet; } @@ -75,12 +77,40 @@ function getStoredTaskCardOptions(card: HTMLElement): Partial { return ((card as any)._taskCardOptions ?? {}) as Partial; } +function parseExpandedRelationshipFilterMode( + value: unknown +): "inherit" | "show-all" { + if (typeof value === "number") { + return value === 1 ? "show-all" : "inherit"; + } + + const normalized = String(value ?? "") + .trim() + .toLowerCase() + .replace(/^['"]|['"]$/g, "") + .replace(/[_\s]+/g, "-"); + + if (normalized === "show-all" || normalized === "1") { + return "show-all"; + } + + if (normalized === "inherit" || normalized === "0") { + return "inherit"; + } + + return "inherit"; +} + function filterExpandedRelationshipTasks( card: HTMLElement, tasks: TaskInfo[] ): TaskInfo[] { const options = getStoredTaskCardOptions(card); - if (options.expandedRelationshipFilterMode !== "inherit") { + const filterMode = parseExpandedRelationshipFilterMode( + options.resolveExpandedRelationshipFilterMode?.() + ?? options.expandedRelationshipFilterMode + ); + if (filterMode !== "inherit") { return tasks; } From 48753e0cd3dc2be0a90c995f8224f6a1a84668e2 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 10:15:29 +1000 Subject: [PATCH 16/29] Fix Pomodoro stats refresh performance --- docs/releases/unreleased.md | 6 + src/services/PomodoroService.ts | 253 +++++++++++++----- src/utils/pomodoroStats.ts | 117 ++++++++ src/views/PomodoroStatsView.ts | 200 ++++---------- src/views/PomodoroView.ts | 33 ++- .../PomodoroService.notifications.test.ts | 70 +++++ .../services/PomodoroService.stats.test.ts | 167 ++++++++++++ tests/unit/utils/pomodoroStats.test.ts | 113 ++++++++ 8 files changed, 725 insertions(+), 234 deletions(-) create mode 100644 src/utils/pomodoroStats.ts create mode 100644 tests/unit/services/PomodoroService.notifications.test.ts create mode 100644 tests/unit/services/PomodoroService.stats.test.ts create mode 100644 tests/unit/utils/pomodoroStats.test.ts diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index bcfcbdb78..080a6439f 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -29,6 +29,12 @@ Example: - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. - Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. +- (#1813) Fixed Pomodoro timer UI stalls caused by refreshing session statistics on every timer tick + - Pomodoro stats now refresh on initial render and session completion instead of once per second + - Pomodoro daily-note stats now read the relevant date/range instead of routing through all history + - Native Pomodoro notifications now respect the notification setting and granted permission before showing + - Thanks to @Szu-Szu for reporting and @its-thex for confirming + - (#1744) Fixed Bases Task List views so changing the per-view `Expanded relationships` option takes effect on re-render - Restores the expected `show-all` behavior for setups that hide subtasks at the top level with `note.projects.isEmpty()` - Thanks to @minol-dev for reporting diff --git a/src/services/PomodoroService.ts b/src/services/PomodoroService.ts index 977607745..303123ded 100644 --- a/src/services/PomodoroService.ts +++ b/src/services/PomodoroService.ts @@ -27,7 +27,14 @@ import { parseDateToLocal, createUTCDateFromLocalCalendarDate, } from "../utils/dateUtils"; -import { getSessionDuration, timerWorker } from "../utils/pomodoroUtils"; +import { timerWorker } from "../utils/pomodoroUtils"; +import { + calculatePomodoroStats, + filterPomodoroSessionsByDateKey, + filterPomodoroSessionsByDateRange, + getPomodoroDateKeysInRange, + sortPomodoroSessions, +} from "../utils/pomodoroStats"; export class PomodoroService { private plugin: TaskNotesPlugin; @@ -268,7 +275,7 @@ export class PomodoroService { } } - new Notification(`Pomodoro started${task ? ` for: ${task.title}` : ""}`); + this.showPomodoroNotification(`Pomodoro started${task ? ` for: ${task.title}` : ""}`); } async startBreak(isLongBreak = false) { @@ -693,17 +700,13 @@ export class PomodoroService { } } - // Show notification - if (this.plugin.settings.pomodoroNotifications) { - const message = - session.type === "work" ? `🍅 Pomodoro completed!` : "☕ Break completed!"; - const body = - session.type === "work" - ? `Time for a ${shouldTakeLongBreak ? "long break 💤" : "short break ☕"}` - : "Ready for the next pomodoro?"; - - new Notification(message, { body }); - } + const message = + session.type === "work" ? `🍅 Pomodoro completed!` : "☕ Break completed!"; + const body = + session.type === "work" + ? `Time for a ${shouldTakeLongBreak ? "long break 💤" : "short break ☕"}` + : "Ready for the next pomodoro?"; + this.showPomodoroNotification(message, { body }); // Play sound if enabled if (this.plugin.settings.pomodoroSoundEnabled) { @@ -804,6 +807,22 @@ export class PomodoroService { } } + private showPomodoroNotification(title: string, options?: NotificationOptions): void { + if (!this.plugin.settings.pomodoroNotifications) { + return; + } + + if (typeof Notification === "undefined" || Notification.permission !== "granted") { + return; + } + + try { + new Notification(title, options); + } catch (error) { + console.warn("Failed to show Pomodoro notification:", error); + } + } + // Public getters getState(): PomodoroState { return { ...this.state }; @@ -926,11 +945,8 @@ export class PomodoroService { // Session History Management async getSessionHistory(): Promise { try { - let history: PomodoroSessionHistory[] = []; - - // Load from plugin data (legacy or current storage) - const data = await this.plugin.loadData(); - const pluginHistory = data?.pomodoroHistory || []; + const pluginHistory = await this.loadPluginHistory(); + let history: PomodoroSessionHistory[]; if (this.plugin.settings.pomodoroStorageLocation === "daily-notes") { // Load from daily notes when that's the primary storage @@ -948,15 +964,68 @@ export class PomodoroService { } // Sort by start time to maintain chronological order - return history.sort( - (a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime() - ); + return sortPomodoroSessions(history); } catch (error) { console.error("Failed to load session history:", error); return []; } } + async getSessionsForDate(date: Date): Promise { + try { + const dateKey = formatDateForStorage(date); + if (!dateKey) { + return []; + } + + const pluginHistory = await this.loadPluginHistoryForDateKey(dateKey); + let history: PomodoroSessionHistory[]; + + if (this.plugin.settings.pomodoroStorageLocation === "daily-notes") { + const dailyNotesHistory = await this.loadHistoryFromDailyNoteForDateKey(dateKey); + history = + pluginHistory.length > 0 + ? this.mergeHistories(pluginHistory, dailyNotesHistory) + : dailyNotesHistory; + } else { + history = pluginHistory; + } + + return sortPomodoroSessions(history); + } catch (error) { + console.error("Failed to load session history for date:", error); + return []; + } + } + + async getSessionsForDateRange( + startDate: Date, + endDate: Date + ): Promise { + try { + const pluginHistory = await this.loadPluginHistoryForDateRange(startDate, endDate); + let history: PomodoroSessionHistory[]; + + if (this.plugin.settings.pomodoroStorageLocation === "daily-notes") { + const dailyNotesHistory = await this.loadHistoryFromDailyNotesForDateRange( + startDate, + endDate + ); + history = + pluginHistory.length > 0 + ? this.mergeHistories(pluginHistory, dailyNotesHistory) + : dailyNotesHistory; + } else { + history = pluginHistory; + } + + return sortPomodoroSessions(history); + } catch (error) { + console.error("Failed to load session history for date range:", error); + return []; + } + } + async saveSessionHistory(history: PomodoroSessionHistory[]): Promise { try { if (this.plugin.settings.pomodoroStorageLocation === "daily-notes") { @@ -1005,45 +1074,18 @@ export class PomodoroService { } async getStatsForDate(date: Date): Promise { - const dateStr = formatDateForStorage(date); - const history = await this.getSessionHistory(); - - // Filter sessions for the specific date - const dayHistory = history.filter((session) => { - const sessionDate = formatDateForStorage(new Date(session.startTime)); - return sessionDate === dateStr; - }); - - // Calculate stats for work sessions only - const workSessions = dayHistory.filter((session) => session.type === "work"); - const completedWork = workSessions.filter((session) => session.completed); + const dayHistory = await this.getSessionsForDate(date); + return calculatePomodoroStats(dayHistory); + } - // Calculate current streak (consecutive completed work sessions from latest backwards) - let currentStreak = 0; - for (let i = workSessions.length - 1; i >= 0; i--) { - if (workSessions[i].completed) { - currentStreak++; - } else { - break; - } - } + async getStatsForDateRange(startDate: Date, endDate: Date): Promise { + const rangeHistory = await this.getSessionsForDateRange(startDate, endDate); + return calculatePomodoroStats(rangeHistory); + } - const totalMinutes = completedWork.reduce( - (sum, session) => sum + getSessionDuration(session), - 0 - ); - const averageSessionLength = - completedWork.length > 0 ? totalMinutes / completedWork.length : 0; - const completionRate = - workSessions.length > 0 ? (completedWork.length / workSessions.length) * 100 : 0; - - return { - pomodorosCompleted: completedWork.length, - currentStreak, - totalMinutes, - averageSessionLength: Math.round(averageSessionLength), - completionRate: Math.round(completionRate), - }; + async getOverallStats(): Promise { + const history = await this.getSessionHistory(); + return calculatePomodoroStats(history); } async getTodayStats(): Promise { @@ -1072,6 +1114,29 @@ export class PomodoroService { this.saveState(); } + private async loadPluginHistory(): Promise { + const data = await this.plugin.loadData(); + const pluginHistory = data?.pomodoroHistory; + return Array.isArray(pluginHistory) ? pluginHistory : []; + } + + private async loadPluginHistoryForDateKey( + dateKey: string + ): Promise { + return filterPomodoroSessionsByDateKey(await this.loadPluginHistory(), dateKey); + } + + private async loadPluginHistoryForDateRange( + startDate: Date, + endDate: Date + ): Promise { + return filterPomodoroSessionsByDateRange( + await this.loadPluginHistory(), + startDate, + endDate + ); + } + /** * Save pomodoro history to daily notes frontmatter */ @@ -1095,6 +1160,58 @@ export class PomodoroService { } } + private async loadHistoryFromDailyNotesForDateRange( + startDate: Date, + endDate: Date + ): Promise { + try { + if (!appHasDailyNotesPluginLoaded()) { + return []; + } + + const allDailyNotes = getAllDailyNotes(); + const history: PomodoroSessionHistory[] = []; + + for (const dateKey of getPomodoroDateKeysInRange(startDate, endDate)) { + const sessions = await this.loadHistoryFromDailyNoteForDateKey( + dateKey, + allDailyNotes + ); + history.push(...sessions); + } + + return history; + } catch (error) { + console.error("Failed to load history from daily notes for date range:", error); + return []; + } + } + + private async loadHistoryFromDailyNoteForDateKey( + dateKey: string, + allDailyNotes?: Record + ): Promise { + try { + if (!dateKey || !appHasDailyNotesPluginLoaded()) { + return []; + } + + const dailyNotes = allDailyNotes ?? getAllDailyNotes(); + const date = parseDateToLocal(dateKey); + const moment = (window as any).moment(date); + const dailyNote = getDailyNote(moment, dailyNotes); + + if (!dailyNote) { + return []; + } + + return this.readPomodoroSessionsFromDailyNote(dailyNote); + } catch (error) { + console.warn(`Failed to load pomodoro history for daily note ${dateKey}:`, error); + return []; + } + } + /** * Load pomodoro history from daily notes frontmatter */ @@ -1107,20 +1224,11 @@ export class PomodoroService { const allHistory: PomodoroSessionHistory[] = []; const allDailyNotes = getAllDailyNotes(); - const pomodoroField = this.plugin.fieldMapper.toUserField("pomodoros"); // Read from each daily note for (const [, file] of Object.entries(allDailyNotes)) { try { - const cache = this.plugin.app.metadataCache.getFileCache(file); - const frontmatter = cache?.frontmatter; - - if (frontmatter && frontmatter[pomodoroField]) { - const sessions = frontmatter[pomodoroField]; - if (Array.isArray(sessions)) { - allHistory.push(...sessions); - } - } + allHistory.push(...this.readPomodoroSessionsFromDailyNote(file)); } catch (error) { console.warn( `Failed to read pomodoro data from daily note ${file.path}:`, @@ -1136,6 +1244,15 @@ export class PomodoroService { } } + private readPomodoroSessionsFromDailyNote(file: any): PomodoroSessionHistory[] { + const cache = this.plugin.app.metadataCache.getFileCache(file); + const frontmatter = cache?.frontmatter; + const pomodoroField = this.plugin.fieldMapper.toUserField("pomodoros"); + const sessions = frontmatter?.[pomodoroField]; + + return Array.isArray(sessions) ? sessions : []; + } + /** * Group sessions by date string (YYYY-MM-DD) */ diff --git a/src/utils/pomodoroStats.ts b/src/utils/pomodoroStats.ts new file mode 100644 index 000000000..a26441ea5 --- /dev/null +++ b/src/utils/pomodoroStats.ts @@ -0,0 +1,117 @@ +import { PomodoroHistoryStats, PomodoroSessionHistory } from "../types"; +import { formatDateForStorage } from "./dateUtils"; +import { getSessionDuration } from "./pomodoroUtils"; + +export function sortPomodoroSessions( + sessions: PomodoroSessionHistory[] +): PomodoroSessionHistory[] { + return [...sessions].sort( + (a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime() + ); +} + +export function getPomodoroSessionDateKey(session: PomodoroSessionHistory): string { + const date = new Date(session.startTime); + + if (isNaN(date.getTime())) { + return ""; + } + + return formatDateForStorage(date); +} + +export function filterPomodoroSessionsByDateKey( + sessions: PomodoroSessionHistory[], + dateKey: string +): PomodoroSessionHistory[] { + if (!dateKey) { + return []; + } + + return sessions.filter((session) => getPomodoroSessionDateKey(session) === dateKey); +} + +export function filterPomodoroSessionsByDate( + sessions: PomodoroSessionHistory[], + date: Date +): PomodoroSessionHistory[] { + return filterPomodoroSessionsByDateKey(sessions, formatDateForStorage(date)); +} + +export function filterPomodoroSessionsByDateRange( + sessions: PomodoroSessionHistory[], + startDate: Date, + endDate: Date +): PomodoroSessionHistory[] { + const startKey = formatDateForStorage(startDate); + const endKey = formatDateForStorage(endDate); + + if (!startKey || !endKey) { + return []; + } + + const [rangeStart, rangeEnd] = startKey <= endKey ? [startKey, endKey] : [endKey, startKey]; + + return sessions.filter((session) => { + const sessionKey = getPomodoroSessionDateKey(session); + return sessionKey >= rangeStart && sessionKey <= rangeEnd; + }); +} + +export function getPomodoroDateKeysInRange(startDate: Date, endDate: Date): string[] { + const startKey = formatDateForStorage(startDate); + const endKey = formatDateForStorage(endDate); + + if (!startKey || !endKey) { + return []; + } + + const [rangeStart, rangeEnd] = startKey <= endKey ? [startKey, endKey] : [endKey, startKey]; + const [startYear, startMonth, startDay] = rangeStart.split("-").map(Number); + const [endYear, endMonth, endDay] = rangeEnd.split("-").map(Number); + const current = new Date(Date.UTC(startYear, startMonth - 1, startDay)); + const end = new Date(Date.UTC(endYear, endMonth - 1, endDay)); + const dateKeys: string[] = []; + + while (current <= end) { + dateKeys.push(formatDateForStorage(current)); + current.setUTCDate(current.getUTCDate() + 1); + } + + return dateKeys; +} + +export function calculatePomodoroStats( + sessions: PomodoroSessionHistory[] +): PomodoroHistoryStats { + const workSessions = sortPomodoroSessions(sessions).filter( + (session) => session.type === "work" + ); + const completedWork = workSessions.filter((session) => session.completed); + + let currentStreak = 0; + for (let i = workSessions.length - 1; i >= 0; i--) { + if (workSessions[i].completed) { + currentStreak++; + } else { + break; + } + } + + const totalMinutes = completedWork.reduce( + (sum, session) => sum + getSessionDuration(session), + 0 + ); + const averageSessionLength = + completedWork.length > 0 ? totalMinutes / completedWork.length : 0; + const completionRate = + workSessions.length > 0 ? (completedWork.length / workSessions.length) * 100 : 0; + + return { + pomodorosCompleted: completedWork.length, + currentStreak, + totalMinutes, + averageSessionLength: Math.round(averageSessionLength), + completionRate: Math.round(completionRate), + }; +} diff --git a/src/views/PomodoroStatsView.ts b/src/views/PomodoroStatsView.ts index a4c6a4179..ad3c4240a 100644 --- a/src/views/PomodoroStatsView.ts +++ b/src/views/PomodoroStatsView.ts @@ -1,13 +1,13 @@ import { ItemView, WorkspaceLeaf, Setting } from "obsidian"; -import { format, startOfWeek, endOfWeek, startOfDay } from "date-fns"; +import { format, startOfWeek, endOfWeek } from "date-fns"; import TaskNotesPlugin from "../main"; import { POMODORO_STATS_VIEW_TYPE, PomodoroHistoryStats, PomodoroSessionHistory } from "../types"; import { - parseTimestamp, getTodayLocal, createUTCDateFromLocalCalendarDate, } from "../utils/dateUtils"; import { getSessionDuration } from "../utils/pomodoroUtils"; +import { calculatePomodoroStats } from "../utils/pomodoroStats"; export class PomodoroStatsView extends ItemView { plugin: TaskNotesPlugin; @@ -40,22 +40,6 @@ export class PomodoroStatsView extends ItemView { return this.plugin.i18n.translate(key, params); } - /** - * Calculate actual duration in minutes with backward compatibility - */ - private calculateActualDuration( - activePeriods: Array<{ startTime: string; endTime?: string }> - ): number { - return activePeriods - .filter((period) => period.endTime) // Only completed periods - .reduce((total, period) => { - const start = new Date(period.startTime); - const end = period.endTime ? new Date(period.endTime) : new Date(); - const durationMs = end.getTime() - start.getTime(); - return total + Math.round(durationMs / (1000 * 60)); // Convert to minutes - }, 0); - } - async onOpen() { await this.plugin.onReady(); await this.render(); @@ -144,85 +128,65 @@ export class PomodoroStatsView extends ItemView { private async refreshStats() { try { - await Promise.all([ - this.updateOverviewStats(), - this.updateTodayStats(), - this.updateWeekStats(), - this.updateOverallStats(), - this.updateRecentSessions(), + const todayLocal = getTodayLocal(); + const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal); + const yesterdayLocal = new Date(todayLocal); + yesterdayLocal.setDate(yesterdayLocal.getDate() - 1); + const yesterdayUTCAnchor = createUTCDateFromLocalCalendarDate(yesterdayLocal); + const firstDaySetting = this.plugin.settings.calendarViewSettings.firstDay || 0; + const weekStartOptions = { + weekStartsOn: firstDaySetting as 0 | 1 | 2 | 3 | 4 | 5 | 6, + }; + const weekStart = startOfWeek(todayUTCAnchor, weekStartOptions); + const weekEnd = endOfWeek(todayUTCAnchor, weekStartOptions); + + const [todayStats, yesterdayStats, weekStats, history] = await Promise.all([ + this.plugin.pomodoroService.getTodayStats(), + this.plugin.pomodoroService.getStatsForDate(yesterdayUTCAnchor), + this.plugin.pomodoroService.getStatsForDateRange(weekStart, weekEnd), + this.plugin.pomodoroService.getSessionHistory(), ]); - } catch (error) { - console.error("Failed to refresh stats:", error); - } - } - - private async updateOverviewStats() { - if (!this.overviewStatsEl) return; - - const todayStats = await this.plugin.pomodoroService.getTodayStats(); - const overallStats = await this.calculateOverallStatsFromHistory(); - - // Use UTC-anchored dates for consistent timezone handling - const todayLocal = getTodayLocal(); - const yesterdayLocal = new Date(todayLocal); - yesterdayLocal.setDate(yesterdayLocal.getDate() - 1); - const yesterdayUTCAnchor = createUTCDateFromLocalCalendarDate(yesterdayLocal); - const yesterdayStats = await this.calculateStatsForRange( - yesterdayUTCAnchor, - yesterdayUTCAnchor - ); - - this.renderOverviewStats(this.overviewStatsEl, todayStats, overallStats, yesterdayStats); - } - - private async updateTodayStats() { - if (!this.todayStatsEl) return; - - const stats = await this.plugin.pomodoroService.getTodayStats(); - this.renderStatsGrid(this.todayStatsEl, stats); - } - - private async updateWeekStats() { - if (!this.weekStatsEl) return; - - // Use UTC-anchored today for consistent timezone handling - const todayLocal = getTodayLocal(); - const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal); - const firstDaySetting = this.plugin.settings.calendarViewSettings.firstDay || 0; - const weekStartOptions = { weekStartsOn: firstDaySetting as 0 | 1 | 2 | 3 | 4 | 5 | 6 }; - const weekStart = startOfWeek(todayUTCAnchor, weekStartOptions); - const weekEnd = endOfWeek(todayUTCAnchor, weekStartOptions); + const overallStats = calculatePomodoroStats(history); + + if (this.overviewStatsEl) { + this.renderOverviewStats( + this.overviewStatsEl, + todayStats, + overallStats, + yesterdayStats + ); + } - const stats = await this.calculateStatsForRange(weekStart, weekEnd); - this.renderStatsGrid(this.weekStatsEl, stats); - } + if (this.todayStatsEl) { + this.renderStatsGrid(this.todayStatsEl, todayStats); + } - private async updateOverallStats() { - if (!this.overallStatsEl) return; + if (this.weekStatsEl) { + this.renderStatsGrid(this.weekStatsEl, weekStats); + } - const history = await this.plugin.pomodoroService.getSessionHistory(); - const stats = this.calculateOverallStats(history); - this.renderStatsGrid(this.overallStatsEl, stats); - } + if (this.overallStatsEl) { + this.renderStatsGrid(this.overallStatsEl, overallStats); + } - private async calculateOverallStatsFromHistory(): Promise { - const history = await this.plugin.pomodoroService.getSessionHistory(); - return this.calculateOverallStats(history); + if (this.recentSessionsEl) { + this.renderRecentSessions(this.recentSessionsEl, history); + } + } catch (error) { + console.error("Failed to refresh stats:", error); + } } - private async updateRecentSessions() { - if (!this.recentSessionsEl) return; - - const history = await this.plugin.pomodoroService.getSessionHistory(); + private renderRecentSessions(container: HTMLElement, history: PomodoroSessionHistory[]) { const recentSessions = history .filter((session: PomodoroSessionHistory) => session.type === "work") .slice(-10) .reverse(); - this.recentSessionsEl.empty(); + container.empty(); if (recentSessions.length === 0) { - this.recentSessionsEl.createDiv({ + container.createDiv({ cls: "pomodoro-no-sessions pomodoro-stats-view__no-sessions", text: this.t("views.pomodoroStats.recents.empty"), }); @@ -230,7 +194,7 @@ export class PomodoroStatsView extends ItemView { } for (const session of recentSessions) { - const sessionEl = this.recentSessionsEl.createDiv({ + const sessionEl = container.createDiv({ cls: "pomodoro-session-item pomodoro-stats-view__session-item", }); @@ -445,72 +409,4 @@ export class PomodoroStatsView extends ItemView { text: this.t("views.pomodoroStats.stats.completion"), }); } - - private async calculateStatsForRange( - startDate: Date, - endDate: Date - ): Promise { - const history = await this.plugin.pomodoroService.getSessionHistory(); - - // Normalize range boundaries to start of day for safe comparison - const normalizedStartDate = startOfDay(startDate); - const normalizedEndDate = startOfDay(endDate); - - // Filter sessions within date range - const rangeSessions = history.filter((session: PomodoroSessionHistory) => { - try { - // Parse the session timestamp safely and normalize to start of day - const sessionTimestamp = parseTimestamp(session.startTime); - const sessionDate = startOfDay(sessionTimestamp); - - // Safe date comparison using normalized dates - return sessionDate >= normalizedStartDate && sessionDate <= normalizedEndDate; - } catch (error) { - console.error("Error parsing session timestamp for filtering:", { - sessionStartTime: session.startTime, - error, - }); - return false; // Exclude sessions with invalid timestamps - } - }); - - return this.calculateStatsFromSessions(rangeSessions); - } - - private calculateOverallStats(history: PomodoroSessionHistory[]): PomodoroHistoryStats { - return this.calculateStatsFromSessions(history); - } - - private calculateStatsFromSessions(sessions: PomodoroSessionHistory[]): PomodoroHistoryStats { - // Filter work sessions only - const workSessions = sessions.filter((session) => session.type === "work"); - const completedWork = workSessions.filter((session) => session.completed); - - // Calculate streak from most recent sessions - let currentStreak = 0; - for (let i = workSessions.length - 1; i >= 0; i--) { - if (workSessions[i].completed) { - currentStreak++; - } else { - break; - } - } - - const totalMinutes = completedWork.reduce( - (sum, session) => sum + getSessionDuration(session), - 0 - ); - const averageSessionLength = - completedWork.length > 0 ? totalMinutes / completedWork.length : 0; - const completionRate = - workSessions.length > 0 ? (completedWork.length / workSessions.length) * 100 : 0; - - return { - pomodorosCompleted: completedWork.length, - currentStreak, - totalMinutes, - averageSessionLength: Math.round(averageSessionLength), - completionRate: Math.round(completionRate), - }; - } } diff --git a/src/views/PomodoroView.ts b/src/views/PomodoroView.ts index 43d1ae094..253f8e0aa 100644 --- a/src/views/PomodoroView.ts +++ b/src/views/PomodoroView.ts @@ -52,6 +52,12 @@ export class PomodoroView extends ItemView { // Event listeners private listeners: EventRef[] = []; + private refreshStats(): void { + this.updateStats().catch((error) => { + console.error("Failed to update stats:", error); + }); + } + constructor(leaf: WorkspaceLeaf, plugin: TaskNotesPlugin) { super(leaf); this.plugin = plugin; @@ -95,12 +101,9 @@ export class PomodoroView extends ItemView { ); this.listeners.push(completeListener); - const interruptListener = this.plugin.emitter.on( - EVENT_POMODORO_INTERRUPT, - ({ session }) => { - this.updateDisplay(); - } - ); + const interruptListener = this.plugin.emitter.on(EVENT_POMODORO_INTERRUPT, () => { + this.updateDisplay(undefined, undefined, { refreshStats: true }); + }); this.listeners.push(interruptListener); const tickListener = this.plugin.emitter.on( @@ -438,9 +441,7 @@ export class PomodoroView extends ItemView { // Initial display update this.updateDisplay(); - this.updateStats().catch((error) => { - console.error("Failed to update initial stats:", error); - }); + this.refreshStats(); // Update initial timer based on current state if (this.plugin.pomodoroService) { @@ -862,7 +863,11 @@ export class PomodoroView extends ItemView { } } - private updateDisplay(session?: PomodoroSession, task?: TaskInfo) { + private updateDisplay( + session?: PomodoroSession, + task?: TaskInfo, + options: { refreshStats?: boolean } = {} + ) { // Check if pomodoroService is available if (!this.plugin.pomodoroService) { // Set default UI state when service is not available @@ -978,9 +983,9 @@ export class PomodoroView extends ItemView { this.subtractTimeButton.removeClass("pomodoro-view__time-adjust-button--hidden"); } - this.updateStats().catch((error) => { - console.error("Failed to update stats:", error); - }); + if (options.refreshStats) { + this.refreshStats(); + } } private updateTimer(seconds: number) { @@ -1123,7 +1128,7 @@ export class PomodoroView extends ItemView { } private onPomodoroComplete(session: PomodoroSession, nextType: string) { - this.updateDisplay(); + this.updateDisplay(undefined, undefined, { refreshStats: true }); // Show completion message and skip break option if (this.statusDisplay) { diff --git a/tests/unit/services/PomodoroService.notifications.test.ts b/tests/unit/services/PomodoroService.notifications.test.ts new file mode 100644 index 000000000..e6a17d2d3 --- /dev/null +++ b/tests/unit/services/PomodoroService.notifications.test.ts @@ -0,0 +1,70 @@ +import { PomodoroService } from "../../../src/services/PomodoroService"; + +function createMockPlugin(pomodoroNotifications: boolean) { + return { + settings: { + pomodoroWorkDuration: 25, + pomodoroNotifications, + }, + i18n: { + translate: jest.fn((key: string) => key), + }, + loadData: jest.fn().mockResolvedValue({}), + saveData: jest.fn().mockResolvedValue(undefined), + emitter: { + trigger: jest.fn(), + }, + taskService: { + startTimeTracking: jest.fn(), + stopTimeTracking: jest.fn(), + }, + } as any; +} + +function installNotificationMock(permission: NotificationPermission) { + const notification = jest.fn() as any; + notification.permission = permission; + Object.defineProperty(globalThis, "Notification", { + configurable: true, + value: notification, + }); + return notification; +} + +describe("PomodoroService notifications", () => { + const originalNotification = globalThis.Notification; + + afterEach(() => { + Object.defineProperty(globalThis, "Notification", { + configurable: true, + value: originalNotification, + }); + }); + + it("does not show native start notifications when Pomodoro notifications are disabled", async () => { + const notification = installNotificationMock("granted"); + const service = new PomodoroService(createMockPlugin(false)); + + await service.startPomodoro(); + + expect(notification).not.toHaveBeenCalled(); + }); + + it("does not show native start notifications without notification permission", async () => { + const notification = installNotificationMock("denied"); + const service = new PomodoroService(createMockPlugin(true)); + + await service.startPomodoro(); + + expect(notification).not.toHaveBeenCalled(); + }); + + it("shows native start notifications when enabled and permission is granted", async () => { + const notification = installNotificationMock("granted"); + const service = new PomodoroService(createMockPlugin(true)); + + await service.startPomodoro(); + + expect(notification).toHaveBeenCalledWith("Pomodoro started", undefined); + }); +}); diff --git a/tests/unit/services/PomodoroService.stats.test.ts b/tests/unit/services/PomodoroService.stats.test.ts new file mode 100644 index 000000000..48d4f3e31 --- /dev/null +++ b/tests/unit/services/PomodoroService.stats.test.ts @@ -0,0 +1,167 @@ +jest.mock("obsidian-daily-notes-interface", () => ({ + appHasDailyNotesPluginLoaded: jest.fn(), + createDailyNote: jest.fn(), + getAllDailyNotes: jest.fn(), + getDailyNote: jest.fn(), +})); + +import { + appHasDailyNotesPluginLoaded, + getAllDailyNotes, + getDailyNote, +} from "obsidian-daily-notes-interface"; +import { PomodoroService } from "../../../src/services/PomodoroService"; +import { PomodoroSessionHistory } from "../../../src/types"; + +const mockedAppHasDailyNotesPluginLoaded = + appHasDailyNotesPluginLoaded as jest.MockedFunction; +const mockedGetAllDailyNotes = getAllDailyNotes as jest.MockedFunction; +const mockedGetDailyNote = getDailyNote as jest.MockedFunction; + +function createSession( + id: string, + startTime: string, + completed = true +): PomodoroSessionHistory { + const start = new Date(startTime); + const end = new Date(start.getTime() + 25 * 60 * 1000).toISOString(); + + return { + id, + startTime, + endTime: end, + plannedDuration: 25, + type: "work", + completed, + activePeriods: [{ startTime, endTime: end }], + }; +} + +function dateKeyFromLocalDate(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function createMockPlugin(options: { + pluginHistory?: PomodoroSessionHistory[]; + dailyNoteSessions?: Record; +}) { + const dailyNoteSessions = options.dailyNoteSessions ?? {}; + const dailyNotes = Object.fromEntries( + Object.keys(dailyNoteSessions).map((dateKey) => [ + dateKey, + { path: `Daily/${dateKey}.md` }, + ]) + ); + const metadataCache = { + getFileCache: jest.fn((file: { path: string }) => { + const dateKey = file.path.match(/(\d{4}-\d{2}-\d{2})/)?.[1] ?? ""; + return { + frontmatter: { + pomodoros: dailyNoteSessions[dateKey] ?? [], + }, + }; + }), + }; + + mockedGetAllDailyNotes.mockReturnValue(dailyNotes as any); + mockedGetDailyNote.mockImplementation((momentValue: any, notes: Record) => { + const date = momentValue?.date instanceof Date ? momentValue.date : momentValue; + return notes[dateKeyFromLocalDate(date)]; + }); + + return { + settings: { + pomodoroWorkDuration: 25, + pomodoroStorageLocation: "daily-notes", + }, + i18n: { + translate: jest.fn((key: string) => key), + }, + loadData: jest.fn().mockResolvedValue({ + pomodoroHistory: options.pluginHistory ?? [], + }), + saveData: jest.fn().mockResolvedValue(undefined), + app: { + metadataCache, + }, + fieldMapper: { + toUserField: jest.fn(() => "pomodoros"), + }, + emitter: { + trigger: jest.fn(), + }, + taskService: { + startTimeTracking: jest.fn(), + stopTimeTracking: jest.fn(), + }, + }; +} + +describe("PomodoroService stats reads", () => { + const originalMoment = (globalThis.window as any).moment; + + beforeEach(() => { + mockedAppHasDailyNotesPluginLoaded.mockReturnValue(true); + (globalThis.window as any).moment = jest.fn((date: Date) => ({ date })); + }); + + afterEach(() => { + (globalThis.window as any).moment = originalMoment; + jest.clearAllMocks(); + }); + + it("loads date stats from the matching daily note instead of scanning all daily notes", async () => { + const targetDate = new Date(Date.UTC(2026, 3, 26)); + const todaySession = createSession("today-daily-note", "2026-04-26T09:00:00.000Z"); + const legacyTodaySession = createSession("today-plugin", "2026-04-26T10:00:00.000Z"); + const legacyYesterdaySession = createSession( + "yesterday-plugin", + "2026-04-25T10:00:00.000Z" + ); + const plugin = createMockPlugin({ + pluginHistory: [legacyTodaySession, legacyYesterdaySession], + dailyNoteSessions: { + "2026-04-24": [createSession("not-in-range", "2026-04-24T09:00:00.000Z")], + "2026-04-26": [todaySession], + }, + }); + const service = new PomodoroService(plugin as any); + const getSessionHistorySpy = jest.spyOn(service, "getSessionHistory"); + + const stats = await service.getStatsForDate(targetDate); + + expect(stats.pomodorosCompleted).toBe(2); + expect(getSessionHistorySpy).not.toHaveBeenCalled(); + expect(plugin.app.metadataCache.getFileCache).toHaveBeenCalledTimes(1); + expect(plugin.app.metadataCache.getFileCache).toHaveBeenCalledWith({ + path: "Daily/2026-04-26.md", + }); + }); + + it("loads range stats from only the daily notes in the requested range", async () => { + const plugin = createMockPlugin({ + dailyNoteSessions: { + "2026-04-24": [createSession("before", "2026-04-24T09:00:00.000Z")], + "2026-04-25": [createSession("start", "2026-04-25T09:00:00.000Z")], + "2026-04-26": [createSession("end", "2026-04-26T09:00:00.000Z")], + "2026-04-27": [createSession("after", "2026-04-27T09:00:00.000Z")], + }, + }); + const service = new PomodoroService(plugin as any); + + const stats = await service.getStatsForDateRange( + new Date(Date.UTC(2026, 3, 25)), + new Date(Date.UTC(2026, 3, 26)) + ); + + expect(stats.pomodorosCompleted).toBe(2); + expect(plugin.app.metadataCache.getFileCache).toHaveBeenCalledTimes(2); + expect(plugin.app.metadataCache.getFileCache.mock.calls.map(([file]) => file.path)).toEqual([ + "Daily/2026-04-25.md", + "Daily/2026-04-26.md", + ]); + }); +}); diff --git a/tests/unit/utils/pomodoroStats.test.ts b/tests/unit/utils/pomodoroStats.test.ts new file mode 100644 index 000000000..2a0aefe05 --- /dev/null +++ b/tests/unit/utils/pomodoroStats.test.ts @@ -0,0 +1,113 @@ +import { + calculatePomodoroStats, + filterPomodoroSessionsByDate, + filterPomodoroSessionsByDateRange, + getPomodoroDateKeysInRange, + sortPomodoroSessions, +} from "../../../src/utils/pomodoroStats"; +import { PomodoroSessionHistory } from "../../../src/types"; + +function session(overrides: Partial): PomodoroSessionHistory { + const startTime = overrides.startTime ?? "2026-04-26T09:00:00.000Z"; + const endTime = overrides.endTime ?? "2026-04-26T09:25:00.000Z"; + + return { + id: overrides.id ?? "session", + startTime, + endTime, + plannedDuration: overrides.plannedDuration ?? 25, + type: overrides.type ?? "work", + completed: overrides.completed ?? true, + taskPath: overrides.taskPath, + activePeriods: + overrides.activePeriods ?? + [ + { + startTime, + endTime, + }, + ], + }; +} + +describe("pomodoroStats", () => { + it("calculates work-session stats from sorted history without view/service duplication", () => { + const sessions = [ + session({ + id: "latest-completed", + startTime: "2026-04-26T12:00:00.000Z", + endTime: "2026-04-26T12:25:00.000Z", + completed: true, + }), + session({ + id: "oldest-completed", + startTime: "2026-04-26T09:00:00.000Z", + endTime: "2026-04-26T09:25:00.000Z", + completed: true, + }), + session({ + id: "break-ignored", + startTime: "2026-04-26T10:00:00.000Z", + endTime: "2026-04-26T10:05:00.000Z", + type: "short-break", + completed: true, + }), + session({ + id: "interrupted", + startTime: "2026-04-26T11:00:00.000Z", + endTime: "2026-04-26T11:10:00.000Z", + completed: false, + }), + ]; + + expect(calculatePomodoroStats(sessions)).toEqual({ + pomodorosCompleted: 2, + currentStreak: 1, + totalMinutes: 50, + averageSessionLength: 25, + completionRate: 67, + }); + }); + + it("filters sessions by UTC storage date and inclusive date ranges", () => { + const sessions = [ + session({ id: "before", startTime: "2026-04-24T23:00:00.000Z" }), + session({ id: "start", startTime: "2026-04-25T09:00:00.000Z" }), + session({ id: "end", startTime: "2026-04-26T09:00:00.000Z" }), + session({ id: "after", startTime: "2026-04-27T09:00:00.000Z" }), + ]; + + expect( + filterPomodoroSessionsByDate( + sessions, + new Date(Date.UTC(2026, 3, 26)) + ).map((item) => item.id) + ).toEqual(["end"]); + expect( + filterPomodoroSessionsByDateRange( + sessions, + new Date(Date.UTC(2026, 3, 25)), + new Date(Date.UTC(2026, 3, 26)) + ).map((item) => item.id) + ).toEqual(["start", "end"]); + expect( + getPomodoroDateKeysInRange( + new Date(Date.UTC(2026, 3, 25)), + new Date(Date.UTC(2026, 3, 27)) + ) + ).toEqual(["2026-04-25", "2026-04-26", "2026-04-27"]); + }); + + it("sorts sessions without mutating the input array", () => { + const sessions = [ + session({ id: "second", startTime: "2026-04-26T10:00:00.000Z" }), + session({ id: "first", startTime: "2026-04-26T09:00:00.000Z" }), + ]; + + expect(sortPomodoroSessions(sessions).map((item) => item.id)).toEqual([ + "first", + "second", + ]); + expect(sessions.map((item) => item.id)).toEqual(["second", "first"]); + }); +}); From e63d9499e330aa0d62e220d39b4adcc246505b74 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 14:12:08 +1000 Subject: [PATCH 17/29] Fix Pomodoro session date bucketing --- docs/releases/unreleased.md | 5 ++ src/services/PomodoroService.ts | 15 ++++-- src/utils/pomodoroStats.ts | 12 ++++- .../services/PomodoroService.stats.test.ts | 46 +++++++++++++++++ tests/unit/utils/pomodoroStats.test.ts | 49 +++++++++++++++++++ 5 files changed, 122 insertions(+), 5 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 080a6439f..b44e962cb 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -29,6 +29,11 @@ Example: - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. - Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. +- (#1658) Fixed Pomodoro stats date bucketing for sessions near local midnight + - Pomodoro session stats now compare the recorded session calendar date against UTC-anchored target days without shifting through UTC or the reader's current timezone + - Pomodoro daily-note storage now writes sessions to the daily note matching the recorded session date + - Thanks to @ewgdg for reporting and @ITblacksheep for PR #1758 + - (#1813) Fixed Pomodoro timer UI stalls caused by refreshing session statistics on every timer tick - Pomodoro stats now refresh on initial render and session completion instead of once per second - Pomodoro daily-note stats now read the relevant date/range instead of routing through all history diff --git a/src/services/PomodoroService.ts b/src/services/PomodoroService.ts index 303123ded..26c925ec0 100644 --- a/src/services/PomodoroService.ts +++ b/src/services/PomodoroService.ts @@ -33,6 +33,7 @@ import { filterPomodoroSessionsByDateKey, filterPomodoroSessionsByDateRange, getPomodoroDateKeysInRange, + getPomodoroSessionDateKey, sortPomodoroSessions, } from "../utils/pomodoroStats"; @@ -1262,8 +1263,11 @@ export class PomodoroService { const grouped = new Map(); for (const session of history) { - const date = new Date(session.startTime); - const dateStr = formatDateForStorage(date); + const dateStr = getPomodoroSessionDateKey(session); + + if (!dateStr) { + continue; + } if (!grouped.has(dateStr)) { grouped.set(dateStr, []); @@ -1279,7 +1283,12 @@ export class PomodoroService { */ private async addSingleSessionToDailyNote(session: PomodoroSessionHistory): Promise { try { - const sessionDate = new Date(session.startTime); + const sessionDateKey = getPomodoroSessionDateKey(session); + if (!sessionDateKey) { + throw new Error(`Invalid Pomodoro session start time: ${session.startTime}`); + } + + const sessionDate = parseDateToLocal(sessionDateKey); const moment = (window as any).moment(sessionDate); // Get or create daily note diff --git a/src/utils/pomodoroStats.ts b/src/utils/pomodoroStats.ts index a26441ea5..d51fa4164 100644 --- a/src/utils/pomodoroStats.ts +++ b/src/utils/pomodoroStats.ts @@ -10,9 +10,13 @@ export function sortPomodoroSessions( ); } -export function getPomodoroSessionDateKey(session: PomodoroSessionHistory): string { - const date = new Date(session.startTime); +export function getPomodoroTimestampDateKey(timestamp: string): string { + const storedCalendarDate = timestamp.match(/^(\d{4}-\d{2}-\d{2})(?:[T\s]|$)/)?.[1]; + if (storedCalendarDate) { + return storedCalendarDate; + } + const date = new Date(timestamp); if (isNaN(date.getTime())) { return ""; } @@ -20,6 +24,10 @@ export function getPomodoroSessionDateKey(session: PomodoroSessionHistory): stri return formatDateForStorage(date); } +export function getPomodoroSessionDateKey(session: PomodoroSessionHistory): string { + return getPomodoroTimestampDateKey(session.startTime); +} + export function filterPomodoroSessionsByDateKey( sessions: PomodoroSessionHistory[], dateKey: string diff --git a/tests/unit/services/PomodoroService.stats.test.ts b/tests/unit/services/PomodoroService.stats.test.ts index 48d4f3e31..73731bde8 100644 --- a/tests/unit/services/PomodoroService.stats.test.ts +++ b/tests/unit/services/PomodoroService.stats.test.ts @@ -65,6 +65,17 @@ function createMockPlugin(options: { }; }), }; + const fileManager = { + processFrontMatter: jest.fn( + async (file: { path: string }, callback: (frontmatter: any) => void) => { + const dateKey = file.path.match(/(\d{4}-\d{2}-\d{2})/)?.[1] ?? ""; + const frontmatter = { + pomodoros: dailyNoteSessions[dateKey] ?? [], + }; + callback(frontmatter); + } + ), + }; mockedGetAllDailyNotes.mockReturnValue(dailyNotes as any); mockedGetDailyNote.mockImplementation((momentValue: any, notes: Record) => { @@ -86,6 +97,7 @@ function createMockPlugin(options: { saveData: jest.fn().mockResolvedValue(undefined), app: { metadataCache, + fileManager, }, fieldMapper: { toUserField: jest.fn(() => "pomodoros"), @@ -164,4 +176,38 @@ describe("PomodoroService stats reads", () => { "Daily/2026-04-26.md", ]); }); + + it("writes completed sessions to the daily note matching the recorded timestamp date", async () => { + const originalTimezone = process.env.TZ; + + try { + process.env.TZ = "Asia/Tokyo"; + const plugin = createMockPlugin({ + dailyNoteSessions: { + "2026-04-02": [], + "2026-04-03": [], + }, + }); + const service = new PomodoroService(plugin as any); + + await service.addSessionToHistory( + createSession("evening-session", "2026-04-02T21:09:25.755-04:00") as any + ); + + expect(plugin.app.fileManager.processFrontMatter).toHaveBeenCalledWith( + { path: "Daily/2026-04-02.md" }, + expect.any(Function) + ); + expect(plugin.app.fileManager.processFrontMatter).not.toHaveBeenCalledWith( + { path: "Daily/2026-04-03.md" }, + expect.any(Function) + ); + } finally { + if (originalTimezone) { + process.env.TZ = originalTimezone; + } else { + delete process.env.TZ; + } + } + }); }); diff --git a/tests/unit/utils/pomodoroStats.test.ts b/tests/unit/utils/pomodoroStats.test.ts index 2a0aefe05..a83b135e5 100644 --- a/tests/unit/utils/pomodoroStats.test.ts +++ b/tests/unit/utils/pomodoroStats.test.ts @@ -2,6 +2,8 @@ import { calculatePomodoroStats, filterPomodoroSessionsByDate, filterPomodoroSessionsByDateRange, + getPomodoroSessionDateKey, + getPomodoroTimestampDateKey, getPomodoroDateKeysInRange, sortPomodoroSessions, } from "../../../src/utils/pomodoroStats"; @@ -98,6 +100,53 @@ describe("pomodoroStats", () => { ).toEqual(["2026-04-25", "2026-04-26", "2026-04-27"]); }); + it("uses the recorded Pomodoro timestamp date instead of shifting through UTC", () => { + const eveningSession = session({ + startTime: "2026-04-02T21:09:25.755-04:00", + endTime: "2026-04-02T21:34:25.755-04:00", + }); + + expect(getPomodoroSessionDateKey(eveningSession)).toBe("2026-04-02"); + expect( + filterPomodoroSessionsByDate( + [eveningSession], + new Date(Date.UTC(2026, 3, 2)) + ).map((item) => item.id) + ).toEqual(["session"]); + expect( + filterPomodoroSessionsByDate( + [eveningSession], + new Date(Date.UTC(2026, 3, 3)) + ).map((item) => item.id) + ).toEqual([]); + }); + + it("keeps stored Pomodoro timestamp dates stable across reader timezones", () => { + const originalTimezone = process.env.TZ; + + try { + for (const timezone of ["America/New_York", "Asia/Tokyo", "Europe/London"]) { + process.env.TZ = timezone; + expect(getPomodoroTimestampDateKey("2026-04-02T21:09:25.755-04:00")).toBe( + "2026-04-02" + ); + } + } finally { + if (originalTimezone) { + process.env.TZ = originalTimezone; + } else { + delete process.env.TZ; + } + } + }); + + it("falls back to UTC storage dates for legacy non-ISO timestamp values", () => { + expect(getPomodoroTimestampDateKey("April 2, 2026 21:09:25 GMT-0400")).toBe( + "2026-04-03" + ); + expect(getPomodoroTimestampDateKey("not a timestamp")).toBe(""); + }); + it("sorts sessions without mutating the input array", () => { const sessions = [ session({ id: "second", startTime: "2026-04-26T10:00:00.000Z" }), From b17fbcea5a56e0fcd6a84e469232085a47de9295 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 14:59:20 +1000 Subject: [PATCH 18/29] Bump NLP core to 0.1.2 --- docs/releases/unreleased.md | 2 ++ package-lock.json | 8 ++--- package.json | 2 +- ...ssue-1667-nlp-standalone-scheduled.test.ts | 33 +++++++++++++++++++ 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 tests/unit/issues/issue-1667-nlp-standalone-scheduled.test.ts diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index b44e962cb..8bee10325 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -29,6 +29,8 @@ Example: - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. - Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. +- (#1667) Fixed NLP scheduled-date parsing so standalone `scheduled` and `start` triggers can set scheduled dates alongside due dates. + - Thanks to @hokfujow for reporting and @UniqueClone for the NLP core PR. - (#1658) Fixed Pomodoro stats date bucketing for sessions near local midnight - Pomodoro session stats now compare the recorded session calendar date against UTC-anchored target days without shifting through UTC or the reader's current timezone - Pomodoro daily-note storage now writes sessions to the daily note matching the recorded session date diff --git a/package-lock.json b/package-lock.json index 5b7c08f95..5d6533c7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "obsidian-daily-notes-interface": "^0.9.4", "reflect-metadata": "^0.2.2", "rrule": "^2.8.1", - "tasknotes-nlp-core": "^0.1.1", + "tasknotes-nlp-core": "^0.1.2", "yaml": "^2.3.1", "zod": "^3.24.0" }, @@ -13082,9 +13082,9 @@ } }, "node_modules/tasknotes-nlp-core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/tasknotes-nlp-core/-/tasknotes-nlp-core-0.1.1.tgz", - "integrity": "sha512-TRc+E6aZ09+Q3g2SQffnnsA7HZqu3aqJFAS5CXqOyiy1/P4RdgiiOyTnwCBNob1EuLo7UbNyHF1zsjdHRljJqQ==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tasknotes-nlp-core/-/tasknotes-nlp-core-0.1.2.tgz", + "integrity": "sha512-qXNLpB0isufCECgRo67mScRLGe8SG8UkX2IN5ofRDNl3hCDXhgY2tV+yoy5t+mmtwJbLz0tcUBwBH+kc10G1Ow==", "license": "MIT", "dependencies": { "chrono-node": "^2.7.5", diff --git a/package.json b/package.json index 4b76e804b..357754e51 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "obsidian-daily-notes-interface": "^0.9.4", "reflect-metadata": "^0.2.2", "rrule": "^2.8.1", - "tasknotes-nlp-core": "^0.1.1", + "tasknotes-nlp-core": "^0.1.2", "yaml": "^2.3.1", "zod": "^3.24.0" } diff --git a/tests/unit/issues/issue-1667-nlp-standalone-scheduled.test.ts b/tests/unit/issues/issue-1667-nlp-standalone-scheduled.test.ts new file mode 100644 index 000000000..0735c9deb --- /dev/null +++ b/tests/unit/issues/issue-1667-nlp-standalone-scheduled.test.ts @@ -0,0 +1,33 @@ +import { NaturalLanguageParser } from "../../../src/services/NaturalLanguageParser"; + +describe("Issue #1667: standalone scheduled NLP triggers", () => { + it("sets scheduled and due dates from standalone scheduled and due triggers", () => { + const parser = new NaturalLanguageParser([], [], false); + + const result = parser.parseInput("Write report scheduled 2026-05-01 due 2026-05-13"); + + expect(result.title).toBe("Write report"); + expect(result.scheduledDate).toBe("2026-05-01"); + expect(result.dueDate).toBe("2026-05-13"); + }); + + it("sets scheduled and due dates from standalone start and due triggers", () => { + const parser = new NaturalLanguageParser([], [], false); + + const result = parser.parseInput("Write report start 2026-05-01 due 2026-05-13"); + + expect(result.title).toBe("Write report"); + expect(result.scheduledDate).toBe("2026-05-01"); + expect(result.dueDate).toBe("2026-05-13"); + }); + + it("does not match scheduled triggers inside longer words", () => { + const parser = new NaturalLanguageParser([], [], false); + + const result = parser.parseInput("Write report started 2026-05-01"); + + expect(result.title).toBe("Write report started"); + expect(result.scheduledDate).toBeUndefined(); + expect(result.dueDate).toBe("2026-05-01"); + }); +}); From 8df11ae794504dc29359fb1ec730239a8a7ca898 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 16:09:42 +1000 Subject: [PATCH 19/29] Document mdbase-tasknotes 0.1.3 release --- docs/releases/unreleased.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 8bee10325..4ae068f09 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -29,6 +29,9 @@ Example: - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. - Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. +- Published `mdbase-tasknotes` 0.1.3 with compatibility fixes for TaskNotes-generated mdbase schemas. + - Includes clearer create-path diagnostics, natural-language `mtn list --due` filters, timer log datetime filters, home-directory path expansion, project wikilink preservation, and correct `mtn --version` reporting. + - Thanks to @tparsons9, @anomatomato, @npondel, @plashal, and @waspeer for the reports and PR. - (#1667) Fixed NLP scheduled-date parsing so standalone `scheduled` and `start` triggers can set scheduled dates alongside due dates. - Thanks to @hokfujow for reporting and @UniqueClone for the NLP core PR. - (#1658) Fixed Pomodoro stats date bucketing for sessions near local midnight From b84dda0db99372f1f4ea0a5e3d8139ffd10f776f Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 16:10:50 +1000 Subject: [PATCH 20/29] Link mdbase-tasknotes in release notes --- docs/releases/unreleased.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 4ae068f09..8fd4b6319 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -29,7 +29,7 @@ Example: - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. - Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. -- Published `mdbase-tasknotes` 0.1.3 with compatibility fixes for TaskNotes-generated mdbase schemas. +- Published [`mdbase-tasknotes`](https://github.com/callumalpass/mdbase-tasknotes) 0.1.3 with compatibility fixes for TaskNotes-generated mdbase schemas. - Includes clearer create-path diagnostics, natural-language `mtn list --due` filters, timer log datetime filters, home-directory path expansion, project wikilink preservation, and correct `mtn --version` reporting. - Thanks to @tparsons9, @anomatomato, @npondel, @plashal, and @waspeer for the reports and PR. - (#1667) Fixed NLP scheduled-date parsing so standalone `scheduled` and `start` triggers can set scheduled dates alongside due dates. From 1e627520a6d304410f5645460f20f542cb563b2a Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 16:43:53 +1000 Subject: [PATCH 21/29] Bump NLP core to 0.1.3 --- docs/releases/unreleased.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 8fd4b6319..8f8802854 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -34,6 +34,7 @@ Example: - Thanks to @tparsons9, @anomatomato, @npondel, @plashal, and @waspeer for the reports and PR. - (#1667) Fixed NLP scheduled-date parsing so standalone `scheduled` and `start` triggers can set scheduled dates alongside due dates. - Thanks to @hokfujow for reporting and @UniqueClone for the NLP core PR. +- Fixed NLP parser title cleanup for explicit date triggers and Japanese/Chinese priority phrases. - (#1658) Fixed Pomodoro stats date bucketing for sessions near local midnight - Pomodoro session stats now compare the recorded session calendar date against UTC-anchored target days without shifting through UTC or the reader's current timezone - Pomodoro daily-note storage now writes sessions to the daily note matching the recorded session date diff --git a/package-lock.json b/package-lock.json index 5d6533c7b..50f9ad6a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "obsidian-daily-notes-interface": "^0.9.4", "reflect-metadata": "^0.2.2", "rrule": "^2.8.1", - "tasknotes-nlp-core": "^0.1.2", + "tasknotes-nlp-core": "^0.1.3", "yaml": "^2.3.1", "zod": "^3.24.0" }, @@ -13082,9 +13082,9 @@ } }, "node_modules/tasknotes-nlp-core": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tasknotes-nlp-core/-/tasknotes-nlp-core-0.1.2.tgz", - "integrity": "sha512-qXNLpB0isufCECgRo67mScRLGe8SG8UkX2IN5ofRDNl3hCDXhgY2tV+yoy5t+mmtwJbLz0tcUBwBH+kc10G1Ow==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/tasknotes-nlp-core/-/tasknotes-nlp-core-0.1.3.tgz", + "integrity": "sha512-qFsL/WL8RsTvLfr8Y/JBGG+AJC/fdr3HgXFRPb8Q3vQbJt6mDmiyH7jpMtzIaHoXJ595FDQNngOH1ZxWgFvspQ==", "license": "MIT", "dependencies": { "chrono-node": "^2.7.5", diff --git a/package.json b/package.json index 357754e51..930cfd307 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "obsidian-daily-notes-interface": "^0.9.4", "reflect-metadata": "^0.2.2", "rrule": "^2.8.1", - "tasknotes-nlp-core": "^0.1.2", + "tasknotes-nlp-core": "^0.1.3", "yaml": "^2.3.1", "zod": "^3.24.0" } From 0b1d688fc81b8e98f0359f97a0708be90da76128 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 16:52:36 +1000 Subject: [PATCH 22/29] Document auto-archive calendar cleanup fix --- docs/releases/unreleased.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 8f8802854..e35a98636 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -26,6 +26,8 @@ Example: ## Fixed +- (#1765, #1769) Fixed auto-archived tasks leaving stale Google Calendar events when cleanup runs before calendar sync is ready or after the task moves into the archive folder. + - Thanks to @martin-forge for reporting and the PR. - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. - Thanks to @martin-forge for the PR and to @jpmoo for reporting the recurring completion issues. - Fixed CI test runs resolving the NLP parser package from a local sibling checkout instead of the published dependency. From 161b2658daf5962b25af2c188b6d29d670cef7e2 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 17:09:02 +1000 Subject: [PATCH 23/29] Fix untranslated locale strings --- docs/releases/unreleased.md | 2 + i18n.state.json | 472 ++++++++++++++++++------------------ src/i18n/resources/de.ts | 42 ++-- src/i18n/resources/es.ts | 74 +++--- src/i18n/resources/fr.ts | 70 +++--- src/i18n/resources/ja.ts | 78 +++--- src/i18n/resources/ko.ts | 64 ++--- src/i18n/resources/pt.ts | 90 ++++--- src/i18n/resources/ru.ts | 30 +-- src/i18n/resources/zh.ts | 52 ++-- 10 files changed, 474 insertions(+), 500 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index e35a98636..fb5eb84d9 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -26,6 +26,8 @@ Example: ## Fixed +- (#884) Fixed untranslated strings and English placeholder examples across non-English interface translations. + - Thanks to @berzernberg for reporting Russian translation gaps. - (#1765, #1769) Fixed auto-archived tasks leaving stale Google Calendar events when cleanup runs before calendar sync is ready or after the task moves into the archive folder. - Thanks to @martin-forge for reporting and the PR. - (#1764) Fixed Google Calendar sync using stale task metadata after rapid task updates, and fixed late recurring completions/skips recording the completion day instead of the scheduled occurrence date. diff --git a/i18n.state.json b/i18n.state.json index 57d8f06f1..3a2bf9c1f 100644 --- a/i18n.state.json +++ b/i18n.state.json @@ -590,11 +590,11 @@ }, "views.basesCalendar.settings.groups.googleCalendars": { "source": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a", - "translation": "567dd668015587a7ed69597e1f6f5a8fc6aff8d9" + "translation": "2edd020ea3d01fdc09b9374ac1839e9b258484bf" }, "views.basesCalendar.settings.groups.microsoftCalendars": { "source": "2edac69ff3c043d662037911af60ea54cc1c3746", - "translation": "0326a4482509b7f06b6e0ebc12bb75df45b01291" + "translation": "2d78acd402e41ccea50d9ae466d7c5dbe3d0ce09" }, "views.basesCalendar.settings.dateNavigation.navigateToDate": { "source": "2a47407705ef9a77de4778c38853a4122e38b1fd", @@ -1218,7 +1218,7 @@ }, "settings.header.documentation": { "source": "9e9cf3221a30246219863f1d2366e36cb580debc", - "translation": "9e9cf3221a30246219863f1d2366e36cb580debc" + "translation": "0401e23e6030c9e8bdb8c202fb8721e3e91ff3a3" }, "settings.header.documentationUrl": { "source": "da82363e11623a8de821a85a3478909ac7ec4fa1", @@ -2962,7 +2962,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "a3d319e8e6001389c53ea0871eb363c0747db606" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -3902,7 +3902,7 @@ }, "settings.integrations.subscriptionsList.typeOptions.remote": { "source": "861ea4efba4bc4f08584c5e9c2b9ae7f681fd930", - "translation": "861ea4efba4bc4f08584c5e9c2b9ae7f681fd930" + "translation": "8ff90e9a92a8c3ceb216912e6ec4059c3517a1bc" }, "settings.integrations.subscriptionsList.typeOptions.local": { "source": "9c144c5d5cfa62acaa96561f0c34f8ff9505e433", @@ -3914,7 +3914,7 @@ }, "settings.integrations.subscriptionsList.placeholders.url": { "source": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05", - "translation": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05" + "translation": "a2aa85b08f2d82237c7016e65eadb185822cf3bf" }, "settings.integrations.subscriptionsList.placeholders.filePath": { "source": "5e0150752956631aec10e6d6c2afa36a8c01f6e7", @@ -4306,7 +4306,7 @@ }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "5eebe96cf4d27de68c373a26609c5f4eebaabd0f" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", @@ -4406,11 +4406,11 @@ }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "3d62432d148d92013350507461a7cf5fff6e0e4d" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "d539a6ab82e96474578334a60fe198421ed1b4a4" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -4474,7 +4474,7 @@ }, "settings.integrations.webhooks.placeholders.url": { "source": "fa7517b6b6b06cccf039eb795c9e0d9184d37002", - "translation": "fa7517b6b6b06cccf039eb795c9e0d9184d37002" + "translation": "279f3c1f7adbe8efd509dd515590754c61e3810e" }, "settings.integrations.webhooks.placeholders.noEventsSelected": { "source": "73427849b05b6cdda2d7a7de8a8e69ca610d8b04", @@ -4782,7 +4782,7 @@ }, "settings.integrations.webhooks.modals.add.url.name": { "source": "fa7517b6b6b06cccf039eb795c9e0d9184d37002", - "translation": "fa7517b6b6b06cccf039eb795c9e0d9184d37002" + "translation": "279f3c1f7adbe8efd509dd515590754c61e3810e" }, "settings.integrations.webhooks.modals.add.url.description": { "source": "7e97eade845fca964072f4491f0b260000ed8ca6", @@ -4882,15 +4882,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "2ebc76bde9fde223aadce2ad115af34fd1b5f8e3" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "b6fc0f38dd097206b6f9ba48bbe550a35a6332c4" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "ebfc6b461b985f416d6c864764157f6a55b2d61c" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -5118,11 +5118,11 @@ }, "modals.deviceCode.title": { "source": "15d49719fa9a20f697d38e6af17a00b8ae4a529d", - "translation": "d9773cd7807685cbb53ffdc1410ab2cbf7b78b85" + "translation": "4ae489e473a5a1d2bf9aaf0efed4a3aed66c8526" }, "modals.deviceCode.instructions.intro": { "source": "6f588f0b7cb402c539b8814f65a4798e8da218e0", - "translation": "042e3c2acbbf4f227d4a3e42887f9ed736baebf3" + "translation": "897bbb5c10219f7cd8e5b0c21d6c3918bfe1c106" }, "modals.deviceCode.steps.open": { "source": "cf9b77061f7b3126b49d50a6fa68f7ca8c26b7a3", @@ -5530,7 +5530,7 @@ }, "modals.icsNoteCreation.templatePathPlaceholder": { "source": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0", - "translation": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0" + "translation": "c1de7730061a52f8bb0679e11ccdf92a9c5c54bc" }, "modals.unscheduledTasksSelector.title": { "source": "1c97535d510286cde7d544601250ab4df5b52f55", @@ -5710,7 +5710,7 @@ }, "modals.task.tagsPlaceholder": { "source": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694", - "translation": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694" + "translation": "62578b900886b4f6ffe6fb8b77fc9d419d49d997" }, "modals.task.timeEstimateLabel": { "source": "ce5a59dd02e94a92ea5c826ad85caaa16db7a8dc", @@ -7934,7 +7934,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "a3d319e8e6001389c53ea0871eb363c0747db606" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -8860,11 +8860,11 @@ }, "views.basesCalendar.settings.groups.googleCalendars": { "source": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a", - "translation": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a" + "translation": "f092e32485daec972ad008629a5e8a9bf808cb46" }, "views.basesCalendar.settings.groups.microsoftCalendars": { "source": "2edac69ff3c043d662037911af60ea54cc1c3746", - "translation": "2edac69ff3c043d662037911af60ea54cc1c3746" + "translation": "ec1cd95d9cd8c101fb7b49ee83eb6ac2613051c4" }, "views.basesCalendar.settings.dateNavigation.navigateToDate": { "source": "2a47407705ef9a77de4778c38853a4122e38b1fd", @@ -9488,7 +9488,7 @@ }, "settings.header.documentation": { "source": "9e9cf3221a30246219863f1d2366e36cb580debc", - "translation": "9e9cf3221a30246219863f1d2366e36cb580debc" + "translation": "60f92f2f9b4fbbfa88b9226f9e42abd30dd32c51" }, "settings.header.documentationUrl": { "source": "da82363e11623a8de821a85a3478909ac7ec4fa1", @@ -11232,7 +11232,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "2c15974747ed06f0aec1a4d5cad454742b31cfae" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -12356,11 +12356,11 @@ }, "settings.integrations.googleCalendarExport.header": { "source": "9e0a0bc16806048547459b3a18c319118b5ac2fe", - "translation": "bffe55366f0229768f0fac7c883f3ae6cbed49be" + "translation": "abb32d18731f96e26b868ea02bddaf56a3a26bd7" }, "settings.integrations.googleCalendarExport.description": { "source": "7bf2b738d7caa0d4b7bda1fe3d2cb8229697d053", - "translation": "1c5f0a6cffc9b23a23abd554e402016812e33c3c" + "translation": "07c2dca9a7336c43c4b84da99c3d878591d36424" }, "settings.integrations.googleCalendarExport.enable.name": { "source": "d13b7fcc23ce56c4596bacd4adcf371d7b51375b", @@ -12368,7 +12368,7 @@ }, "settings.integrations.googleCalendarExport.enable.description": { "source": "833a82630d348f84d1ffffd1fdf6edea47ae9348", - "translation": "45c266cbd9b0249011840cf53bbc05cc47dd44e0" + "translation": "fe871bd97649463a5b602aaef887924aadc3c721" }, "settings.integrations.googleCalendarExport.targetCalendar.name": { "source": "038cb9973aca6b47e77437690d0db87eda040948", @@ -12384,7 +12384,7 @@ }, "settings.integrations.googleCalendarExport.targetCalendar.connectFirst": { "source": "ed44f996f5d6d975172c0b447ea79f9c4c68070c", - "translation": "5e21eb2b5109b1c86a8c3ba1ccb8552e3b77e5aa" + "translation": "ae1e056d60d7ef8db5462cb17a6927f6ab3d10b3" }, "settings.integrations.googleCalendarExport.targetCalendar.primarySuffix": { "source": "6aa7a8a5d67e1af219adc10b5a90323cba411343", @@ -12460,7 +12460,7 @@ }, "settings.integrations.googleCalendarExport.defaultReminder.description": { "source": "766fa723249615aeebf7d1cfe9b78e935bef79b6", - "translation": "0472bf7734896b6db50aa78b3d7509eea81624ba" + "translation": "b496e864f7fe7ffa0172933b72a7e89d981bb7db" }, "settings.integrations.googleCalendarExport.automaticSyncBehavior.header": { "source": "937fe30dfe4713c5d91f84d395517664cf8010ab", @@ -12508,7 +12508,7 @@ }, "settings.integrations.googleCalendarExport.syncAllTasks.description": { "source": "78cc848b82544b3e885314da90643b3c669d15fb", - "translation": "b35cb8920ff11fea069c71e0866cb584c2ef84cc" + "translation": "27051298f4c10d03447cecedd3bdbc354d596a4c" }, "settings.integrations.googleCalendarExport.syncAllTasks.buttonText": { "source": "870265e59f8fb9eccf9517600487190be187e0dd", @@ -12540,11 +12540,11 @@ }, "settings.integrations.googleCalendarExport.notices.notEnabled": { "source": "7c29daa48f8202207d12928dc8ee7167e719aebc", - "translation": "4d567b61694b7d218a3209acd249631a8599875f" + "translation": "21d4f5a1224afd55cae8e4b2fe95af44991116dc" }, "settings.integrations.googleCalendarExport.notices.notEnabledOrConfigured": { "source": "92cade59912ab66a0e3ff572d03ebe725cad1356", - "translation": "9c0b9816e27df66d0a573c5e7c608a1a9845e6ce" + "translation": "8ba85ce9afab8493aaa9f79ae4305db9a356ea25" }, "settings.integrations.googleCalendarExport.notices.serviceNotAvailable": { "source": "8aa1556edc129b64f58d4a4e21a9f9966c3b8396", @@ -12556,7 +12556,7 @@ }, "settings.integrations.googleCalendarExport.notices.taskSynced": { "source": "002e68af1aae01f1c0fdcc551308dbdf4589e124", - "translation": "d47e0156f9b418c667c84677eb82448c4f8accbe" + "translation": "49bc00bc13aee78b964296fe374e62af1493fb57" }, "settings.integrations.googleCalendarExport.notices.noActiveFile": { "source": "75d16ace81b583f789e31ebd123030e22a8ea8dc", @@ -12572,15 +12572,15 @@ }, "settings.integrations.googleCalendarExport.notices.syncFailed": { "source": "e260cf9e4f67bc503e435d6cd753b53f8284a087", - "translation": "36f5fedb80209c5a392142701c1d189a4b81ee39" + "translation": "c914a8878886b205a39762eb24b65d4cda551136" }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "b7841f9b7d28573dcc3772e5181b0534bab5ada9" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", - "translation": "9ade27e5f156dec6b71b2089dcff8f29429d858c" + "translation": "6ef8dc2902d864f13ed57f57581ba155a3a98c7a" }, "settings.integrations.googleCalendarExport.notices.syncComplete": { "source": "7cf19e37c30b10260d12ec55585fd7098e28f4ae", @@ -12676,11 +12676,11 @@ }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "cf9fc24bbaf5ab33ed3ff8fc2b4a8ed0642163fe" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "de80c6ff0dcf123b646783a09bdd281ce7263536" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -13152,15 +13152,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "7fd9a8ce788bd086cd773985ae5c7c4f64a0a549" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "e503e991ef813b0bb3c2581f8951b5b2eb5d9f26" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "ef875c2cc49e12517f43866969d0cefb860f339e" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -13364,11 +13364,11 @@ }, "commands.syncAllTasksGoogleCalendar": { "source": "a142233976ac0191e2a4d0221f7a9cd24054e7b3", - "translation": "dcb9ec19b07b18757e5e42111e989a52efddbe51" + "translation": "5840b0229d932d02e001c43f80f20fa8eb1a7845" }, "commands.syncCurrentTaskGoogleCalendar": { "source": "b3a2c7b2038e709301e677d8c1070ee0b8b257a1", - "translation": "f3a68bf0429447bc8bcf2c1538ece4485c96387e" + "translation": "3ed42167300178735bf00da3b2b1cb7e958591fc" }, "commands.viewReleaseNotes": { "source": "04c2072af97cf70c22fbf9efa9b62eb2574d23d6", @@ -13388,11 +13388,11 @@ }, "modals.deviceCode.title": { "source": "15d49719fa9a20f697d38e6af17a00b8ae4a529d", - "translation": "69974268e1a96961aa677e2cbc376113498dcce8" + "translation": "b751828a1280ee6986bb981e927b7233e89f770f" }, "modals.deviceCode.instructions.intro": { "source": "6f588f0b7cb402c539b8814f65a4798e8da218e0", - "translation": "b1d676ed51a6ea6fbf2c59103f71bcd486a2c23b" + "translation": "e2c5f032c1754be77c3a8fb38480d0812a5f67d6" }, "modals.deviceCode.steps.open": { "source": "cf9b77061f7b3126b49d50a6fa68f7ca8c26b7a3", @@ -13800,7 +13800,7 @@ }, "modals.icsNoteCreation.templatePathPlaceholder": { "source": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0", - "translation": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0" + "translation": "17fe8f993ad5f2fad9d1b312b8ac8bba6e8167e4" }, "modals.unscheduledTasksSelector.title": { "source": "1c97535d510286cde7d544601250ab4df5b52f55", @@ -14860,15 +14860,15 @@ }, "contextMenus.task.calendar.google": { "source": "570374e4e4cc42f26e069bb5d791b5bbe89e4068", - "translation": "570374e4e4cc42f26e069bb5d791b5bbe89e4068" + "translation": "a894b57ee68e7bf4ca7abbb5f18123f8d6a83ce6" }, "contextMenus.task.calendar.outlook": { "source": "fc2327823b94fa65b207e92718f76e162abe08f4", - "translation": "fc2327823b94fa65b207e92718f76e162abe08f4" + "translation": "f61d7a58753564a261bcf5590ee3f5447cfc83e9" }, "contextMenus.task.calendar.yahoo": { "source": "120ca537dd07874066d335e71f9a0df171d8ec28", - "translation": "120ca537dd07874066d335e71f9a0df171d8ec28" + "translation": "b146bbd87df268ca7f043de231b06ffe850ad7a2" }, "contextMenus.task.calendar.downloadIcs": { "source": "8b180d4c410e3d9062bd65d6dbefe626eec813d8", @@ -14876,19 +14876,19 @@ }, "contextMenus.task.calendar.syncToGoogle": { "source": "6bcb0757c7198a7caa3dd59c635a4b5555559938", - "translation": "cd8ca9855e6747678159676646846c0e71c9c156" + "translation": "0b818ffa44b0fcd92f45ac6e1a954de9ead0e087" }, "contextMenus.task.calendar.syncToGoogleNotConfigured": { "source": "f8de13fc800a02f249186b43c628d89f6c69b9d9", - "translation": "7392f0c32bdacfdbf7c6886428811dd00cd567f4" + "translation": "aefc736f6f7ad9ab941fbad69a11b4407acb4438" }, "contextMenus.task.calendar.syncToGoogleSuccess": { "source": "002e68af1aae01f1c0fdcc551308dbdf4589e124", - "translation": "7a94b8dfeeea560ef2375f60fccc0464f4673c3d" + "translation": "40b9bc37e26c947a451b2fb4cdd7ac0fca37aa5a" }, "contextMenus.task.calendar.syncToGoogleFailed": { "source": "cf874861c171987e28af7f10e4174836f724ed27", - "translation": "cac0657f04b646c916739ab4326ea78b7ec5114e" + "translation": "34bcf63e4e3e7f74d9c19d25fdd2ddccc39e4987" }, "contextMenus.task.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -15716,7 +15716,7 @@ }, "ui.taskCard.googleCalendarSyncTooltip": { "source": "9ff5d85645b7b7bcfb1d12fe5c66ecba0487b1ca", - "translation": "555c9932e8930ad817864d0acf758d2891e40cc4" + "translation": "1540958591a43c5110a4f279c181edb674e33c6f" }, "ui.propertyEventCard.unknownFile": { "source": "b05d96bfbcef633c0d11626390d994c13bbf58d0", @@ -16204,7 +16204,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "2c15974747ed06f0aec1a4d5cad454742b31cfae" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -17130,11 +17130,11 @@ }, "views.basesCalendar.settings.groups.googleCalendars": { "source": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a", - "translation": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a" + "translation": "87288b88188cb773f5a27f8828b1f3f8de0a3c50" }, "views.basesCalendar.settings.groups.microsoftCalendars": { "source": "2edac69ff3c043d662037911af60ea54cc1c3746", - "translation": "2edac69ff3c043d662037911af60ea54cc1c3746" + "translation": "ee7114e6852e694f46938134d56836dbac94ea3b" }, "views.basesCalendar.settings.dateNavigation.navigateToDate": { "source": "2a47407705ef9a77de4778c38853a4122e38b1fd", @@ -19502,7 +19502,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "bb811af8d75cbd9f303475a810b90a8df0ee5bb5" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -20626,11 +20626,11 @@ }, "settings.integrations.googleCalendarExport.header": { "source": "9e0a0bc16806048547459b3a18c319118b5ac2fe", - "translation": "0c35cf28cf8c176cb308f835477e61fbabd8bad3" + "translation": "d231af5d1306ed74e3916500a39af6a46a9ab3d3" }, "settings.integrations.googleCalendarExport.description": { "source": "7bf2b738d7caa0d4b7bda1fe3d2cb8229697d053", - "translation": "5bb0569b156542f461aaec814859c86ea0267a16" + "translation": "da6f9cad5d1e1b6f263608e5d79d67a0aa5d7941" }, "settings.integrations.googleCalendarExport.enable.name": { "source": "d13b7fcc23ce56c4596bacd4adcf371d7b51375b", @@ -20638,7 +20638,7 @@ }, "settings.integrations.googleCalendarExport.enable.description": { "source": "833a82630d348f84d1ffffd1fdf6edea47ae9348", - "translation": "504d7cc7862abb71596a96cc51e9b22ec4b012d0" + "translation": "43ab58f71c34f1e062ce6208a257d066a7171326" }, "settings.integrations.googleCalendarExport.targetCalendar.name": { "source": "038cb9973aca6b47e77437690d0db87eda040948", @@ -20654,7 +20654,7 @@ }, "settings.integrations.googleCalendarExport.targetCalendar.connectFirst": { "source": "ed44f996f5d6d975172c0b447ea79f9c4c68070c", - "translation": "c59148a58fa4ff60fbeb84c9d7fe2d8a2a325c30" + "translation": "c929c997308e2e1869ee6006abca80a1996196c4" }, "settings.integrations.googleCalendarExport.targetCalendar.primarySuffix": { "source": "6aa7a8a5d67e1af219adc10b5a90323cba411343", @@ -20730,7 +20730,7 @@ }, "settings.integrations.googleCalendarExport.defaultReminder.description": { "source": "766fa723249615aeebf7d1cfe9b78e935bef79b6", - "translation": "505aaac2824c6ae74fb9f3e83241f1eb8ac3bae4" + "translation": "f9c128fb487dd7c29486259c889cb05cfe574e89" }, "settings.integrations.googleCalendarExport.automaticSyncBehavior.header": { "source": "937fe30dfe4713c5d91f84d395517664cf8010ab", @@ -20778,7 +20778,7 @@ }, "settings.integrations.googleCalendarExport.syncAllTasks.description": { "source": "78cc848b82544b3e885314da90643b3c669d15fb", - "translation": "5d29cd5ada090bdb1d19f698ba32f0d0e3c1cd2e" + "translation": "ee502102dff76459fbdfb20dd50c9a9450c10717" }, "settings.integrations.googleCalendarExport.syncAllTasks.buttonText": { "source": "870265e59f8fb9eccf9517600487190be187e0dd", @@ -20810,11 +20810,11 @@ }, "settings.integrations.googleCalendarExport.notices.notEnabled": { "source": "7c29daa48f8202207d12928dc8ee7167e719aebc", - "translation": "cfe3c2b2988eec3700e504539eb9bb35715dbbd2" + "translation": "2a327c60d7e4f6d0b64fdafa1269e9dd3ca1031e" }, "settings.integrations.googleCalendarExport.notices.notEnabledOrConfigured": { "source": "92cade59912ab66a0e3ff572d03ebe725cad1356", - "translation": "50f09934dba6fdbc683b6c1f3a6a7f8fef22420a" + "translation": "c5c0886bc588c2ea96a36ad6c2f9a776d97bb6de" }, "settings.integrations.googleCalendarExport.notices.serviceNotAvailable": { "source": "8aa1556edc129b64f58d4a4e21a9f9966c3b8396", @@ -20826,7 +20826,7 @@ }, "settings.integrations.googleCalendarExport.notices.taskSynced": { "source": "002e68af1aae01f1c0fdcc551308dbdf4589e124", - "translation": "ae430e4235f55917f361971a112a8e01863ae965" + "translation": "573a2748c0b626f424c5fa8fba5a6f9ee5972476" }, "settings.integrations.googleCalendarExport.notices.noActiveFile": { "source": "75d16ace81b583f789e31ebd123030e22a8ea8dc", @@ -20842,15 +20842,15 @@ }, "settings.integrations.googleCalendarExport.notices.syncFailed": { "source": "e260cf9e4f67bc503e435d6cd753b53f8284a087", - "translation": "cf98643b82e793b6184e2098c89301f70316a59e" + "translation": "ebb133ad08cae921251b75bc97e632609f49a635" }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "c6ae50b915d7fadb7c361de2ca602e013fa5a49d" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", - "translation": "733a4d1f58cdf8379212d4d8b6fc6353d0dbfbd2" + "translation": "f1f3ecb4317e427a097f68a1a7dd4d25ab98b2cd" }, "settings.integrations.googleCalendarExport.notices.syncComplete": { "source": "7cf19e37c30b10260d12ec55585fd7098e28f4ae", @@ -20946,11 +20946,11 @@ }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "ddac067aded4023f161b7c3ba9c0e7b46a1a5b1b" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "0a2b68d220e48440112253e7e7c0354b6f3022fe" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -21422,15 +21422,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "2f345a699a67a52a372eda0b52ece21495d1f7f5" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "4504794663d5d575827777ad7db35315607d4609" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "aec57eb1e62a2212d0ee9b3612708a7a50f0a2ba" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -21634,11 +21634,11 @@ }, "commands.syncAllTasksGoogleCalendar": { "source": "a142233976ac0191e2a4d0221f7a9cd24054e7b3", - "translation": "2fac7132b39d51e7eab81c737acd139ccaf1b156" + "translation": "28640215f1b03cb801ffb8dc756830c98f42e3a4" }, "commands.syncCurrentTaskGoogleCalendar": { "source": "b3a2c7b2038e709301e677d8c1070ee0b8b257a1", - "translation": "8687822ff26b8cd109b9ea28c74eac2f1457cdbc" + "translation": "a72b2c5cc268d540831c322fc114fc0960da450f" }, "commands.viewReleaseNotes": { "source": "04c2072af97cf70c22fbf9efa9b62eb2574d23d6", @@ -21658,11 +21658,11 @@ }, "modals.deviceCode.title": { "source": "15d49719fa9a20f697d38e6af17a00b8ae4a529d", - "translation": "95908c6ccf03e20fef6c5c1c2d9628e08e3a2d4b" + "translation": "3fd7f1097ffdd1ec563ab5714dcdee521c89ec5f" }, "modals.deviceCode.instructions.intro": { "source": "6f588f0b7cb402c539b8814f65a4798e8da218e0", - "translation": "cc6d264d46c10d13039f9c76495ebbd818bd6956" + "translation": "648d029acaafe340954b7a254e277f2f9c9d93c4" }, "modals.deviceCode.steps.open": { "source": "cf9b77061f7b3126b49d50a6fa68f7ca8c26b7a3", @@ -22070,7 +22070,7 @@ }, "modals.icsNoteCreation.templatePathPlaceholder": { "source": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0", - "translation": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0" + "translation": "cc37b118a415b0c3be384873c2cc248096c80656" }, "modals.unscheduledTasksSelector.title": { "source": "1c97535d510286cde7d544601250ab4df5b52f55", @@ -23134,11 +23134,11 @@ }, "contextMenus.task.calendar.outlook": { "source": "fc2327823b94fa65b207e92718f76e162abe08f4", - "translation": "ec7adb1576bc1d64d09ec28732373ad1bd6e8d8e" + "translation": "ea933ce2b31e5635a802f22db5cc0015b684e33c" }, "contextMenus.task.calendar.yahoo": { "source": "120ca537dd07874066d335e71f9a0df171d8ec28", - "translation": "bc67ac511aa90df5b1b9d6c9a6b1244f0d6eba15" + "translation": "2b17a97550a103f135a2466cc757151d4026378e" }, "contextMenus.task.calendar.downloadIcs": { "source": "8b180d4c410e3d9062bd65d6dbefe626eec813d8", @@ -23146,19 +23146,19 @@ }, "contextMenus.task.calendar.syncToGoogle": { "source": "6bcb0757c7198a7caa3dd59c635a4b5555559938", - "translation": "a6a7149184b062fcb3bebc50ecd2ebe81207e3d0" + "translation": "a36efc3bb7d4b4176250a0bac2f4b069cf830a4b" }, "contextMenus.task.calendar.syncToGoogleNotConfigured": { "source": "f8de13fc800a02f249186b43c628d89f6c69b9d9", - "translation": "732096ef7dc6e8fcae04be21519f9c6a8d4cb940" + "translation": "ee4523321de09b1582abff4a09df263dc33ddb29" }, "contextMenus.task.calendar.syncToGoogleSuccess": { "source": "002e68af1aae01f1c0fdcc551308dbdf4589e124", - "translation": "fdeb86433c0ee04cdfe5fe96f6c9b0f94d12ae01" + "translation": "26726ca96345869579aa24c350cf9041ebdc798a" }, "contextMenus.task.calendar.syncToGoogleFailed": { "source": "cf874861c171987e28af7f10e4174836f724ed27", - "translation": "b06ad7af70f4a4e694b559108cc76abd35d77578" + "translation": "02277fdd5360b4c6b808b996179dfd63a682156a" }, "contextMenus.task.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -23986,7 +23986,7 @@ }, "ui.taskCard.googleCalendarSyncTooltip": { "source": "9ff5d85645b7b7bcfb1d12fe5c66ecba0487b1ca", - "translation": "1ec1c8d3ab94baaba510a25f4350caebb507a12f" + "translation": "53a21c82a92b9e1a8c2b9b2686b00987ae057e7c" }, "ui.propertyEventCard.unknownFile": { "source": "b05d96bfbcef633c0d11626390d994c13bbf58d0", @@ -24474,7 +24474,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "bb811af8d75cbd9f303475a810b90a8df0ee5bb5" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -25400,11 +25400,11 @@ }, "views.basesCalendar.settings.groups.googleCalendars": { "source": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a", - "translation": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a" + "translation": "47c1185f53d1312f2e65f49918f7dca64d1deb35" }, "views.basesCalendar.settings.groups.microsoftCalendars": { "source": "2edac69ff3c043d662037911af60ea54cc1c3746", - "translation": "2edac69ff3c043d662037911af60ea54cc1c3746" + "translation": "d1fcbc3503e0e64af998bf82ca1aad3afbe4a1a6" }, "views.basesCalendar.settings.dateNavigation.navigateToDate": { "source": "2a47407705ef9a77de4778c38853a4122e38b1fd", @@ -26028,7 +26028,7 @@ }, "settings.header.documentation": { "source": "9e9cf3221a30246219863f1d2366e36cb580debc", - "translation": "9e9cf3221a30246219863f1d2366e36cb580debc" + "translation": "427e87e11f2aee5d1161c1d76a4b55d46588413c" }, "settings.header.documentationUrl": { "source": "da82363e11623a8de821a85a3478909ac7ec4fa1", @@ -26508,7 +26508,7 @@ }, "settings.defaults.basicDefaults.defaultContexts.placeholder": { "source": "eb5351be12fa774b4a50a1a81f0d4ad1dbad6469", - "translation": "eb5351be12fa774b4a50a1a81f0d4ad1dbad6469" + "translation": "1cbd5a94abc846d6c8297d575948e83a257f7b73" }, "settings.defaults.basicDefaults.defaultTags.name": { "source": "270b0f369b5f266751f63c8385d02eb0b0af52f9", @@ -26520,7 +26520,7 @@ }, "settings.defaults.basicDefaults.defaultTags.placeholder": { "source": "9edb91dcdc2ed1b8bd5dc3f07584a82bc839de55", - "translation": "9edb91dcdc2ed1b8bd5dc3f07584a82bc839de55" + "translation": "285acaeca9f3536f285d8cbdc8cf6043f2fa08bd" }, "settings.defaults.basicDefaults.defaultProjects.name": { "source": "59b2ab51cbab743f865ef605d839a2ca89509f3f", @@ -26704,7 +26704,7 @@ }, "settings.defaults.bodyTemplate.bodyTemplateFile.placeholder": { "source": "505e5277ec9adf731c105828e0f2939e6bc165fe", - "translation": "505e5277ec9adf731c105828e0f2939e6bc165fe" + "translation": "5d2bf46ee428af67adc340aafcaf6451b77cc206" }, "settings.defaults.bodyTemplate.bodyTemplateFile.ariaLabel": { "source": "2869220f1deea766831798736f09706780b721f4", @@ -27396,7 +27396,7 @@ }, "settings.taskProperties.taskStatuses.placeholders.value": { "source": "23766ca6dac0b9ef9b1f220c3305262d21be9c21", - "translation": "23766ca6dac0b9ef9b1f220c3305262d21be9c21" + "translation": "c573867b5fca81967f6487423d702786c72051b9" }, "settings.taskProperties.taskStatuses.placeholders.label": { "source": "f61eadaf153a7ccf995f48d19a000c62df62384c", @@ -27476,7 +27476,7 @@ }, "settings.taskProperties.taskPriorities.placeholders.value": { "source": "9235afd3e98802411861a961aa9cf61e90c1c977", - "translation": "9235afd3e98802411861a961aa9cf61e90c1c977" + "translation": "b096b3f5acd628b9535183af5c243631423a6259" }, "settings.taskProperties.taskPriorities.placeholders.label": { "source": "c9ccd692742b394172624fce3b7d795099797618", @@ -27668,7 +27668,7 @@ }, "settings.taskProperties.customUserFields.placeholders.propertyKey": { "source": "52ab656f5995d7d6458361c5531afcee9f3ed69a", - "translation": "52ab656f5995d7d6458361c5531afcee9f3ed69a" + "translation": "99076334e91d62ab016682267981ce827a120b10" }, "settings.taskProperties.customUserFields.placeholders.defaultValue": { "source": "bdffe654f8554300a98d2acefcbbf1d894339b2b", @@ -27704,7 +27704,7 @@ }, "settings.taskProperties.customUserFields.defaultNames.noKey": { "source": "576426d52f216a70835ec62c0172ee7f762f8199", - "translation": "576426d52f216a70835ec62c0172ee7f762f8199" + "translation": "6f8c643a0783c0308922700936260fc24f0b996e" }, "settings.taskProperties.customUserFields.deleteTooltip": { "source": "5ac477c267332c84ac6f5fe5509d523e08a4af94", @@ -27772,7 +27772,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "b867baf64967423f97db4b50b3b355dc741b0fa3" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -28240,7 +28240,7 @@ }, "settings.appearance.projectAutosuggest.requiredTags.placeholder": { "source": "a775fd915accfb6c2637dbea2dee57088c975a5c", - "translation": "a775fd915accfb6c2637dbea2dee57088c975a5c" + "translation": "e99e09d729d6edf0d2fe7f65b8192114e6a44c35" }, "settings.appearance.projectAutosuggest.includeFolders.name": { "source": "5507cbfd472d676840b7abd4f3714ff49d5279aa", @@ -28252,7 +28252,7 @@ }, "settings.appearance.projectAutosuggest.includeFolders.placeholder": { "source": "4ec67f231cf21897bf5b9a1390979d7d5d8eec7d", - "translation": "4ec67f231cf21897bf5b9a1390979d7d5d8eec7d" + "translation": "029ce26ccffa21fff4a8c280cf7977c1abc14f80" }, "settings.appearance.projectAutosuggest.requiredPropertyKey.name": { "source": "bd40523bc90fdd1c0ecb19c3f745c47cd4f040d2", @@ -28264,7 +28264,7 @@ }, "settings.appearance.projectAutosuggest.requiredPropertyKey.placeholder": { "source": "d0a3e7f81a9885e99049d1cae0336d269d5e47a9", - "translation": "d0a3e7f81a9885e99049d1cae0336d269d5e47a9" + "translation": "3fc53d516938a86fba7eb635f4647254bd4d93ab" }, "settings.appearance.projectAutosuggest.requiredPropertyValue.name": { "source": "41788e0dbabcac0f45a127b9a15c50823fbe911e", @@ -28276,7 +28276,7 @@ }, "settings.appearance.projectAutosuggest.requiredPropertyValue.placeholder": { "source": "98f54143ab4e86b28c3afee0f50f2f51cfb2ed38", - "translation": "98f54143ab4e86b28c3afee0f50f2f51cfb2ed38" + "translation": "598ab78312d81dc734208e1af4111acd9de08fac" }, "settings.appearance.projectAutosuggest.customizeDisplay.name": { "source": "8a7210b95c69b2a52b0ce87041695adfa1ec77e7", @@ -28308,7 +28308,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row1.placeholder": { "source": "398d2038b85f84f7a4c973f0a6682363b6892f12", - "translation": "398d2038b85f84f7a4c973f0a6682363b6892f12" + "translation": "3360643b09b20e6c3814fcb6c4aaf05ce068ab59" }, "settings.appearance.projectAutosuggest.displayRows.row2.name": { "source": "67d0756ccdcc47818b53ebc1f5579ac77c550fe2", @@ -28320,7 +28320,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row2.placeholder": { "source": "51b7434e9885414ebb0d1440edc7aca00395e5fb", - "translation": "51b7434e9885414ebb0d1440edc7aca00395e5fb" + "translation": "a6f2079b9f41d64b0da5cfa4c97df653bc475f21" }, "settings.appearance.projectAutosuggest.displayRows.row3.name": { "source": "14696a15f0ca131b3e5a75b6496e7bf258410cff", @@ -28332,7 +28332,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row3.placeholder": { "source": "37885afc8c11c54b8beaae2d2b9106587ee01b47", - "translation": "37885afc8c11c54b8beaae2d2b9106587ee01b47" + "translation": "06bf9a5b1fe6055883304ed0af6cfe69a661ebc1" }, "settings.appearance.projectAutosuggest.quickReference.header": { "source": "c1477a08bbc119a706dea90b93268ceb957f433a", @@ -28544,7 +28544,7 @@ }, "settings.integrations.calendarSubscriptions.defaultNoteTemplate.placeholder": { "source": "407271b9437780b13207e94cd8e15555445fd2c9", - "translation": "407271b9437780b13207e94cd8e15555445fd2c9" + "translation": "01ba88e6f3d8b92b334d0a30872e32fce7b71f1a" }, "settings.integrations.calendarSubscriptions.defaultNoteFolder.name": { "source": "725e47db9f5f753d6888917e2cf31326086c5e1c", @@ -28556,7 +28556,7 @@ }, "settings.integrations.calendarSubscriptions.defaultNoteFolder.placeholder": { "source": "1c2841e501fd1c8368c9c2fefed7bd1e3d62acc1", - "translation": "1c2841e501fd1c8368c9c2fefed7bd1e3d62acc1" + "translation": "0ab513006df55c26447b8340f0489931724f9670" }, "settings.integrations.calendarSubscriptions.filenameFormat.name": { "source": "52ddeb943a8fdb7b6fbfc5aa22da1f36e38a493c", @@ -28724,7 +28724,7 @@ }, "settings.integrations.subscriptionsList.placeholders.url": { "source": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05", - "translation": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05" + "translation": "f3b9775747dbaad73b09fa0d1602ad1d798586c5" }, "settings.integrations.subscriptionsList.placeholders.filePath": { "source": "5e0150752956631aec10e6d6c2afa36a8c01f6e7", @@ -28732,7 +28732,7 @@ }, "settings.integrations.subscriptionsList.placeholders.localFile": { "source": "519edd5d8a5071ef8699535df1f1530b0ebb328a", - "translation": "519edd5d8a5071ef8699535df1f1530b0ebb328a" + "translation": "03729980bde1f0bc3823562a407ada65e70b6de3" }, "settings.integrations.subscriptionsList.statusLabels.enabled": { "source": "df174a3f2faa31814e06540acda7af8825403fac", @@ -28816,7 +28816,7 @@ }, "settings.integrations.autoExport.filePath.placeholder": { "source": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9", - "translation": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9" + "translation": "13ecf529a60e03e074b79bc136ce671cf0386578" }, "settings.integrations.autoExport.interval.name": { "source": "556715bd2aecbd24fb83bc1f8cef895487c2d007", @@ -29116,7 +29116,7 @@ }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "af602d87357ecdea122d2abdc33ff8e1231e6815" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", @@ -29212,15 +29212,15 @@ }, "settings.integrations.httpApi.authToken.placeholder": { "source": "dfd8e9adf613d1b2a46dfac755c50174ccecef4c", - "translation": "dfd8e9adf613d1b2a46dfac755c50174ccecef4c" + "translation": "e8d93404df7e13401d44c85b36b6b66ab9242ba0" }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "a1844678f57a09891167dd2153b179c9994e6f28" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "17219a9dc25121a68893c3b9359cf964ff03e211" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -29284,7 +29284,7 @@ }, "settings.integrations.webhooks.placeholders.url": { "source": "fa7517b6b6b06cccf039eb795c9e0d9184d37002", - "translation": "fa7517b6b6b06cccf039eb795c9e0d9184d37002" + "translation": "fb96f3d5d55d0ea98fbae45af28be3960900e2a6" }, "settings.integrations.webhooks.placeholders.noEventsSelected": { "source": "73427849b05b6cdda2d7a7de8a8e69ca610d8b04", @@ -29592,7 +29592,7 @@ }, "settings.integrations.webhooks.modals.add.url.name": { "source": "fa7517b6b6b06cccf039eb795c9e0d9184d37002", - "translation": "fa7517b6b6b06cccf039eb795c9e0d9184d37002" + "translation": "fb96f3d5d55d0ea98fbae45af28be3960900e2a6" }, "settings.integrations.webhooks.modals.add.url.description": { "source": "7e97eade845fca964072f4491f0b260000ed8ca6", @@ -29692,15 +29692,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "bdf3046eb079ff776e4d3cfac72c439bd43b789b" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "535be58eceda8f215f968f44317afbabe28fd881" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "b57acce5acce6d6fd92e8dfc978103091127dec0" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -30340,7 +30340,7 @@ }, "modals.icsNoteCreation.templatePathPlaceholder": { "source": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0", - "translation": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0" + "translation": "12e94992fa79a6235b3dc4f2c294f5a7a14de1e1" }, "modals.unscheduledTasksSelector.title": { "source": "1c97535d510286cde7d544601250ab4df5b52f55", @@ -30512,7 +30512,7 @@ }, "modals.task.contextsPlaceholder": { "source": "ed25bc348d5d2e3aa87e1d6137ae17dcfdf27e00", - "translation": "ed25bc348d5d2e3aa87e1d6137ae17dcfdf27e00" + "translation": "dfc8b30fb04d8793c0f8478e26566244202802a2" }, "modals.task.tagsLabel": { "source": "848eed0fbd5429f556b2982dec3ea87136e33e44", @@ -30520,7 +30520,7 @@ }, "modals.task.tagsPlaceholder": { "source": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694", - "translation": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694" + "translation": "384fc6a1ae443122d7d6ae72fb10ec2885ffde28" }, "modals.task.timeEstimateLabel": { "source": "ce5a59dd02e94a92ea5c826ad85caaa16db7a8dc", @@ -30700,7 +30700,7 @@ }, "modals.task.userFields.listPlaceholder": { "source": "7631d4338d79002f6b387619bb4c0c29a3a1b7bb", - "translation": "7631d4338d79002f6b387619bb4c0c29a3a1b7bb" + "translation": "ec988a1ce6599913584bb3cb2deedbf06edf3c28" }, "modals.task.userFields.pickDate": { "source": "1efc8ee6cdc1e9089a679e3ea26bc4805d1c57a1", @@ -32744,7 +32744,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "b867baf64967423f97db4b50b3b355dc741b0fa3" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -34778,7 +34778,7 @@ }, "settings.defaults.basicDefaults.defaultContexts.placeholder": { "source": "eb5351be12fa774b4a50a1a81f0d4ad1dbad6469", - "translation": "eb5351be12fa774b4a50a1a81f0d4ad1dbad6469" + "translation": "6fc41cfb9621c93f807493a08a12a78d07c940cd" }, "settings.defaults.basicDefaults.defaultTags.name": { "source": "270b0f369b5f266751f63c8385d02eb0b0af52f9", @@ -34790,7 +34790,7 @@ }, "settings.defaults.basicDefaults.defaultTags.placeholder": { "source": "9edb91dcdc2ed1b8bd5dc3f07584a82bc839de55", - "translation": "9edb91dcdc2ed1b8bd5dc3f07584a82bc839de55" + "translation": "b587db1ed2a9db9c2210d977d7a6b8acaf38c110" }, "settings.defaults.basicDefaults.defaultProjects.name": { "source": "59b2ab51cbab743f865ef605d839a2ca89509f3f", @@ -34974,7 +34974,7 @@ }, "settings.defaults.bodyTemplate.bodyTemplateFile.placeholder": { "source": "505e5277ec9adf731c105828e0f2939e6bc165fe", - "translation": "505e5277ec9adf731c105828e0f2939e6bc165fe" + "translation": "df293d240404cc05d3e67a0c6b3595c1046b602c" }, "settings.defaults.bodyTemplate.bodyTemplateFile.ariaLabel": { "source": "2869220f1deea766831798736f09706780b721f4", @@ -35666,7 +35666,7 @@ }, "settings.taskProperties.taskStatuses.placeholders.value": { "source": "23766ca6dac0b9ef9b1f220c3305262d21be9c21", - "translation": "23766ca6dac0b9ef9b1f220c3305262d21be9c21" + "translation": "0dae9079ff6d7f2bb03c630aa3e65d34c16fbb90" }, "settings.taskProperties.taskStatuses.placeholders.label": { "source": "f61eadaf153a7ccf995f48d19a000c62df62384c", @@ -35746,7 +35746,7 @@ }, "settings.taskProperties.taskPriorities.placeholders.value": { "source": "9235afd3e98802411861a961aa9cf61e90c1c977", - "translation": "9235afd3e98802411861a961aa9cf61e90c1c977" + "translation": "962636f483b354327c696d2b9d73fb2862c0f76e" }, "settings.taskProperties.taskPriorities.placeholders.label": { "source": "c9ccd692742b394172624fce3b7d795099797618", @@ -35938,7 +35938,7 @@ }, "settings.taskProperties.customUserFields.placeholders.propertyKey": { "source": "52ab656f5995d7d6458361c5531afcee9f3ed69a", - "translation": "52ab656f5995d7d6458361c5531afcee9f3ed69a" + "translation": "e737302323bb8e1eb6a70f05a66f036b53febeb7" }, "settings.taskProperties.customUserFields.placeholders.defaultValue": { "source": "bdffe654f8554300a98d2acefcbbf1d894339b2b", @@ -35974,7 +35974,7 @@ }, "settings.taskProperties.customUserFields.defaultNames.noKey": { "source": "576426d52f216a70835ec62c0172ee7f762f8199", - "translation": "576426d52f216a70835ec62c0172ee7f762f8199" + "translation": "75720d4643d81c7cca86586d21959299d22e7365" }, "settings.taskProperties.customUserFields.deleteTooltip": { "source": "5ac477c267332c84ac6f5fe5509d523e08a4af94", @@ -36042,7 +36042,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "4a2cbb13cacee0c13e6509009b34b4323c8878fe" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -36510,7 +36510,7 @@ }, "settings.appearance.projectAutosuggest.requiredTags.placeholder": { "source": "a775fd915accfb6c2637dbea2dee57088c975a5c", - "translation": "a775fd915accfb6c2637dbea2dee57088c975a5c" + "translation": "47ed1bdef15679a69bc452c269d5b087e47a5904" }, "settings.appearance.projectAutosuggest.includeFolders.name": { "source": "5507cbfd472d676840b7abd4f3714ff49d5279aa", @@ -36522,7 +36522,7 @@ }, "settings.appearance.projectAutosuggest.includeFolders.placeholder": { "source": "4ec67f231cf21897bf5b9a1390979d7d5d8eec7d", - "translation": "4ec67f231cf21897bf5b9a1390979d7d5d8eec7d" + "translation": "6bea6747041da9abdb0d45416ceb2d8c3209b996" }, "settings.appearance.projectAutosuggest.requiredPropertyKey.name": { "source": "bd40523bc90fdd1c0ecb19c3f745c47cd4f040d2", @@ -36534,7 +36534,7 @@ }, "settings.appearance.projectAutosuggest.requiredPropertyKey.placeholder": { "source": "d0a3e7f81a9885e99049d1cae0336d269d5e47a9", - "translation": "d0a3e7f81a9885e99049d1cae0336d269d5e47a9" + "translation": "80577c5f499ea0c68e091b38589cd0ac12aa200e" }, "settings.appearance.projectAutosuggest.requiredPropertyValue.name": { "source": "41788e0dbabcac0f45a127b9a15c50823fbe911e", @@ -36546,7 +36546,7 @@ }, "settings.appearance.projectAutosuggest.requiredPropertyValue.placeholder": { "source": "98f54143ab4e86b28c3afee0f50f2f51cfb2ed38", - "translation": "98f54143ab4e86b28c3afee0f50f2f51cfb2ed38" + "translation": "65281a189635d891eacffef53dd841d76301f6ff" }, "settings.appearance.projectAutosuggest.customizeDisplay.name": { "source": "8a7210b95c69b2a52b0ce87041695adfa1ec77e7", @@ -36578,7 +36578,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row1.placeholder": { "source": "398d2038b85f84f7a4c973f0a6682363b6892f12", - "translation": "398d2038b85f84f7a4c973f0a6682363b6892f12" + "translation": "e09a93b4642e0c97b8c78c239c324477b07d6f6e" }, "settings.appearance.projectAutosuggest.displayRows.row2.name": { "source": "67d0756ccdcc47818b53ebc1f5579ac77c550fe2", @@ -36590,7 +36590,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row2.placeholder": { "source": "51b7434e9885414ebb0d1440edc7aca00395e5fb", - "translation": "51b7434e9885414ebb0d1440edc7aca00395e5fb" + "translation": "88175c77ca20c2d4ce888d20c88e9c784678ae53" }, "settings.appearance.projectAutosuggest.displayRows.row3.name": { "source": "14696a15f0ca131b3e5a75b6496e7bf258410cff", @@ -36602,7 +36602,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row3.placeholder": { "source": "37885afc8c11c54b8beaae2d2b9106587ee01b47", - "translation": "37885afc8c11c54b8beaae2d2b9106587ee01b47" + "translation": "f4f9443f68971010762f9c32a81fae6e99c700e1" }, "settings.appearance.projectAutosuggest.quickReference.header": { "source": "c1477a08bbc119a706dea90b93268ceb957f433a", @@ -36814,7 +36814,7 @@ }, "settings.integrations.calendarSubscriptions.defaultNoteTemplate.placeholder": { "source": "407271b9437780b13207e94cd8e15555445fd2c9", - "translation": "407271b9437780b13207e94cd8e15555445fd2c9" + "translation": "82ad3cdfb5ef13d17d37d3288cd1cbe96418404e" }, "settings.integrations.calendarSubscriptions.defaultNoteFolder.name": { "source": "725e47db9f5f753d6888917e2cf31326086c5e1c", @@ -36826,7 +36826,7 @@ }, "settings.integrations.calendarSubscriptions.defaultNoteFolder.placeholder": { "source": "1c2841e501fd1c8368c9c2fefed7bd1e3d62acc1", - "translation": "1c2841e501fd1c8368c9c2fefed7bd1e3d62acc1" + "translation": "a173bbd2e11af3111c9e0633b66dbe50f172d045" }, "settings.integrations.calendarSubscriptions.filenameFormat.name": { "source": "52ddeb943a8fdb7b6fbfc5aa22da1f36e38a493c", @@ -36994,7 +36994,7 @@ }, "settings.integrations.subscriptionsList.placeholders.url": { "source": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05", - "translation": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05" + "translation": "2b9f49d625e41ea0f883e520e808eb4c0b3162ac" }, "settings.integrations.subscriptionsList.placeholders.filePath": { "source": "5e0150752956631aec10e6d6c2afa36a8c01f6e7", @@ -37002,7 +37002,7 @@ }, "settings.integrations.subscriptionsList.placeholders.localFile": { "source": "519edd5d8a5071ef8699535df1f1530b0ebb328a", - "translation": "519edd5d8a5071ef8699535df1f1530b0ebb328a" + "translation": "f9ce693cacb31e6a590d7550511e1f0b1be6ad79" }, "settings.integrations.subscriptionsList.statusLabels.enabled": { "source": "df174a3f2faa31814e06540acda7af8825403fac", @@ -37086,7 +37086,7 @@ }, "settings.integrations.autoExport.filePath.placeholder": { "source": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9", - "translation": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9" + "translation": "e53e142ff20d10924a96d12c248c1a4dd2b78578" }, "settings.integrations.autoExport.interval.name": { "source": "556715bd2aecbd24fb83bc1f8cef895487c2d007", @@ -37386,7 +37386,7 @@ }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "4f194f30b3dbff7c941e7ad207701f670302e9ef" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", @@ -37482,15 +37482,15 @@ }, "settings.integrations.httpApi.authToken.placeholder": { "source": "dfd8e9adf613d1b2a46dfac755c50174ccecef4c", - "translation": "dfd8e9adf613d1b2a46dfac755c50174ccecef4c" + "translation": "a7e751d48f382c535ad55ee337d2f63dafe1fb73" }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "0b2025de762dc4d4e475bc0f3ed758210dd818b3" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "a669f8e39f8efa0e0355bf4315560150d95a3b8a" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -37962,15 +37962,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "5c2dc0c54d2da30af544ab02e5a59d5e383c5241" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "b00132dbabeb57102936bb2a9a799e922e766401" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "42ded92793032f7a7916222635afcd83b6bbe1c2" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -38570,7 +38570,7 @@ }, "modals.icsNoteCreation.folderPlaceholder": { "source": "c7cbb4b45068905aef0f0961bdfb1c7fe9988b52", - "translation": "c7cbb4b45068905aef0f0961bdfb1c7fe9988b52" + "translation": "7c6479499abbacbb756cecb98f64b28b81bac877" }, "modals.icsNoteCreation.createButton": { "source": "6e157c5da4410b7e9de85f5c93026b9176e69064", @@ -38610,7 +38610,7 @@ }, "modals.icsNoteCreation.templatePathPlaceholder": { "source": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0", - "translation": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0" + "translation": "f40b9f22af9f79c4e7b42f1c280e88faa9020f24" }, "modals.unscheduledTasksSelector.title": { "source": "1c97535d510286cde7d544601250ab4df5b52f55", @@ -38782,7 +38782,7 @@ }, "modals.task.contextsPlaceholder": { "source": "ed25bc348d5d2e3aa87e1d6137ae17dcfdf27e00", - "translation": "ed25bc348d5d2e3aa87e1d6137ae17dcfdf27e00" + "translation": "46215aa6f67600482f0a978c28ffab4eac591e43" }, "modals.task.tagsLabel": { "source": "848eed0fbd5429f556b2982dec3ea87136e33e44", @@ -38790,7 +38790,7 @@ }, "modals.task.tagsPlaceholder": { "source": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694", - "translation": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694" + "translation": "d4ddbd693b9653d2d87cc13691cd2e2bb5257ad9" }, "modals.task.timeEstimateLabel": { "source": "ce5a59dd02e94a92ea5c826ad85caaa16db7a8dc", @@ -41014,7 +41014,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "4a2cbb13cacee0c13e6509009b34b4323c8878fe" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -41940,11 +41940,11 @@ }, "views.basesCalendar.settings.groups.googleCalendars": { "source": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a", - "translation": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a" + "translation": "19e55941caf9ef86e3fa38e5be8ca9f845e12693" }, "views.basesCalendar.settings.groups.microsoftCalendars": { "source": "2edac69ff3c043d662037911af60ea54cc1c3746", - "translation": "2edac69ff3c043d662037911af60ea54cc1c3746" + "translation": "d0def7be25f86c5ffac9600ebd776119dc16c0a9" }, "views.basesCalendar.settings.dateNavigation.navigateToDate": { "source": "2a47407705ef9a77de4778c38853a4122e38b1fd", @@ -42568,7 +42568,7 @@ }, "settings.header.documentation": { "source": "9e9cf3221a30246219863f1d2366e36cb580debc", - "translation": "9e9cf3221a30246219863f1d2366e36cb580debc" + "translation": "7ee3f754f8c51477442e8aae4964e38fac6921da" }, "settings.header.documentationUrl": { "source": "da82363e11623a8de821a85a3478909ac7ec4fa1", @@ -43688,7 +43688,7 @@ }, "settings.taskProperties.properties.dateCreated.description": { "source": "a1883ed7d61c98487374e27115f587c69e42f8ab", - "translation": "c0696c09c088b14ed846046d83c0929eb86d2f18" + "translation": "67775d17ecdeb1ae17403fd89f05a0cd63aa1e5f" }, "settings.taskProperties.properties.dateModified.name": { "source": "f20bd941fd9d88bd37f6bef52b729756ad7f1545", @@ -43696,7 +43696,7 @@ }, "settings.taskProperties.properties.dateModified.description": { "source": "4ef47658b8bc1c0b1eaa0d7cbd2ecd612696fc72", - "translation": "1f2de77d7f1895bcd219b724c40e0a4ab2012fc0" + "translation": "08fa029401a15d3784ec7bdb41f8d80e839e1bcf" }, "settings.taskProperties.properties.completedDate.name": { "source": "0089d3b136526c1dfbe0487bc7c3c8ef99812909", @@ -43704,7 +43704,7 @@ }, "settings.taskProperties.properties.completedDate.description": { "source": "73d1b0db7dc181c5c4a44ad779426e033241b030", - "translation": "d4c2a2dbd1d32fa086e66ced88db89d00c14afc4" + "translation": "d9e1291c6bebbf096ede5703f9ca0aab413609a2" }, "settings.taskProperties.properties.archiveTag.name": { "source": "6cbf3083f89024980523b94958aa8e3adc3337d2", @@ -43720,7 +43720,7 @@ }, "settings.taskProperties.properties.timeEntries.description": { "source": "42707e6d67884d64ef73dac9067a7dee663c3945", - "translation": "c7642a03947a9368862cf0a7ef1ff2dacf93572a" + "translation": "9c0c8e52a52d208c1135b20ef00d5a9974c23d1a" }, "settings.taskProperties.properties.completeInstances.name": { "source": "0bdad37998da001877c73de475592a9d4df98714", @@ -44312,7 +44312,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "4963608df7eb2f8ad5791063eecc5066695114e7" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -44384,7 +44384,7 @@ }, "settings.appearance.taskFilenames.filenameFormat.options.timestamp": { "source": "3e93fff4c0704e6ec6f66da90214b4e8faadda61", - "translation": "21d9bc3c841f507fef513aecf00b735235b66891" + "translation": "c83b8b326d55192e928d6e30f04449c0ec1436ef" }, "settings.appearance.taskFilenames.filenameFormat.options.custom": { "source": "ee5ac739c10d86287af5928583fd7de10a587fd6", @@ -45116,7 +45116,7 @@ }, "settings.integrations.calendarSubscriptions.filenameFormat.options.timestamp": { "source": "19eabc961735d78f12fc7be906ffcb033853cf85", - "translation": "19eabc961735d78f12fc7be906ffcb033853cf85" + "translation": "62c948c4253d060333258dd5540ab192aeae14ae" }, "settings.integrations.calendarSubscriptions.filenameFormat.options.custom": { "source": "ee5ac739c10d86287af5928583fd7de10a587fd6", @@ -45356,7 +45356,7 @@ }, "settings.integrations.autoExport.filePath.placeholder": { "source": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9", - "translation": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9" + "translation": "31f536b3e8d8dd136b9b77d58544cb9eb448f48f" }, "settings.integrations.autoExport.interval.name": { "source": "556715bd2aecbd24fb83bc1f8cef895487c2d007", @@ -45436,11 +45436,11 @@ }, "settings.integrations.googleCalendarExport.header": { "source": "9e0a0bc16806048547459b3a18c319118b5ac2fe", - "translation": "7c6d2d7635d5fe548373ddabae534dae0d0af9fd" + "translation": "0015bccda7fce65912d30b88714f53debc6bcede" }, "settings.integrations.googleCalendarExport.description": { "source": "7bf2b738d7caa0d4b7bda1fe3d2cb8229697d053", - "translation": "d282b9651d9a215c755a122e27d20552c444f6a7" + "translation": "b393885920d041135d98e8ac8c83aee344ff1309" }, "settings.integrations.googleCalendarExport.enable.name": { "source": "d13b7fcc23ce56c4596bacd4adcf371d7b51375b", @@ -45448,7 +45448,7 @@ }, "settings.integrations.googleCalendarExport.enable.description": { "source": "833a82630d348f84d1ffffd1fdf6edea47ae9348", - "translation": "8f6a5202b93ee99c1c7876cf06386c4221c97de2" + "translation": "93737244617d2107524f9118ff27fe21661a1cea" }, "settings.integrations.googleCalendarExport.targetCalendar.name": { "source": "038cb9973aca6b47e77437690d0db87eda040948", @@ -45464,7 +45464,7 @@ }, "settings.integrations.googleCalendarExport.targetCalendar.connectFirst": { "source": "ed44f996f5d6d975172c0b447ea79f9c4c68070c", - "translation": "23982fc108a1b5d0e06af7f871c684ca4e6c7b8e" + "translation": "e73fa9d9b7e8649ba82271f270278621bc147f98" }, "settings.integrations.googleCalendarExport.targetCalendar.primarySuffix": { "source": "6aa7a8a5d67e1af219adc10b5a90323cba411343", @@ -45540,7 +45540,7 @@ }, "settings.integrations.googleCalendarExport.defaultReminder.description": { "source": "766fa723249615aeebf7d1cfe9b78e935bef79b6", - "translation": "874acd6285ab407885c2a3973aa32868210eb42c" + "translation": "b9ccad09e1d2532ae2c26e181816316b305c0ce1" }, "settings.integrations.googleCalendarExport.automaticSyncBehavior.header": { "source": "937fe30dfe4713c5d91f84d395517664cf8010ab", @@ -45588,7 +45588,7 @@ }, "settings.integrations.googleCalendarExport.syncAllTasks.description": { "source": "78cc848b82544b3e885314da90643b3c669d15fb", - "translation": "e83ab8127776d3496885d68a529b0ed05c859416" + "translation": "8fa369ea9f114659d42f0888203f18ee3eabf538" }, "settings.integrations.googleCalendarExport.syncAllTasks.buttonText": { "source": "870265e59f8fb9eccf9517600487190be187e0dd", @@ -45620,11 +45620,11 @@ }, "settings.integrations.googleCalendarExport.notices.notEnabled": { "source": "7c29daa48f8202207d12928dc8ee7167e719aebc", - "translation": "545dd218ac97063de9fc464662112d2ddb9d92db" + "translation": "57bd3bd0a177e1d9e3806f448ff0cde559ace2ea" }, "settings.integrations.googleCalendarExport.notices.notEnabledOrConfigured": { "source": "92cade59912ab66a0e3ff572d03ebe725cad1356", - "translation": "97d76beb310cdd5b82bb8afd43cb05114f823ffc" + "translation": "10c4646d02f394bf4ff7576c27534c811f493aac" }, "settings.integrations.googleCalendarExport.notices.serviceNotAvailable": { "source": "8aa1556edc129b64f58d4a4e21a9f9966c3b8396", @@ -45636,7 +45636,7 @@ }, "settings.integrations.googleCalendarExport.notices.taskSynced": { "source": "002e68af1aae01f1c0fdcc551308dbdf4589e124", - "translation": "6445a10915de8ce7d4665fb60e0a2f93a877ba53" + "translation": "c015cd211f611282ba45f299d2ecc8f1209cd474" }, "settings.integrations.googleCalendarExport.notices.noActiveFile": { "source": "75d16ace81b583f789e31ebd123030e22a8ea8dc", @@ -45652,15 +45652,15 @@ }, "settings.integrations.googleCalendarExport.notices.syncFailed": { "source": "e260cf9e4f67bc503e435d6cd753b53f8284a087", - "translation": "1707144d30124dd65bbaef57b093b762805af3cd" + "translation": "3444258f3d651c09fac13bae9f1492f0c52a04df" }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "44174ca6208149bf1cceca9f130abf56136aebfd" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", - "translation": "fa4fe08ec4d8255a48cbe5da23b7e4739f1da8d9" + "translation": "a7887b0689db7e1db371088d1e344886cfb2a751" }, "settings.integrations.googleCalendarExport.notices.syncComplete": { "source": "7cf19e37c30b10260d12ec55585fd7098e28f4ae", @@ -45756,11 +45756,11 @@ }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "cd4e28f9c8a82999c2f5018a66b506f08797312d" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "fa2544dc41797945998edb82004508a6b0f6e1e5" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -46232,15 +46232,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "cfafdd3be6c1be69bf944f4c56e0f5b59ee371a4" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "d7d76e86302ba213d5a09eaeb547e1e6fcf7702d" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "d727aedb73b9a418d4f1a360053b98e5125af01f" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -46444,11 +46444,11 @@ }, "commands.syncAllTasksGoogleCalendar": { "source": "a142233976ac0191e2a4d0221f7a9cd24054e7b3", - "translation": "16fdd3694c0ee4eef431998a40960994353c7fd9" + "translation": "7cd83f6cce0ee4ba9504a050d0a511511f24aa6b" }, "commands.syncCurrentTaskGoogleCalendar": { "source": "b3a2c7b2038e709301e677d8c1070ee0b8b257a1", - "translation": "9ab149e86042a1c68a5d80792536279fdac57195" + "translation": "dc339de24650421ffc37cd5bbce5fba92128651a" }, "commands.viewReleaseNotes": { "source": "04c2072af97cf70c22fbf9efa9b62eb2574d23d6", @@ -46468,11 +46468,11 @@ }, "modals.deviceCode.title": { "source": "15d49719fa9a20f697d38e6af17a00b8ae4a529d", - "translation": "70487189f54d0d36192b055d8fe4de61e26d2d97" + "translation": "593a2468a8efaddbd38aadc53f01098d23309cd8" }, "modals.deviceCode.instructions.intro": { "source": "6f588f0b7cb402c539b8814f65a4798e8da218e0", - "translation": "b8cdf78648a404bcffec8f7ad62e219795407cab" + "translation": "59772d60b04805e303dad2cb8adf25168ce93f6e" }, "modals.deviceCode.steps.open": { "source": "cf9b77061f7b3126b49d50a6fa68f7ca8c26b7a3", @@ -47060,7 +47060,7 @@ }, "modals.task.tagsPlaceholder": { "source": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694", - "translation": "d4f8c9916b6b36cae1e28d9ec466d7abe3aa8694" + "translation": "381b755934d4445d0c57c240abec794951eceb0c" }, "modals.task.timeEstimateLabel": { "source": "ce5a59dd02e94a92ea5c826ad85caaa16db7a8dc", @@ -47240,7 +47240,7 @@ }, "modals.task.userFields.listPlaceholder": { "source": "7631d4338d79002f6b387619bb4c0c29a3a1b7bb", - "translation": "7631d4338d79002f6b387619bb4c0c29a3a1b7bb" + "translation": "d352f25a4ce866fd982c1dcb4191f2a4b2b5b2a4" }, "modals.task.userFields.pickDate": { "source": "1efc8ee6cdc1e9089a679e3ea26bc4805d1c57a1", @@ -47940,15 +47940,15 @@ }, "contextMenus.task.calendar.google": { "source": "570374e4e4cc42f26e069bb5d791b5bbe89e4068", - "translation": "570374e4e4cc42f26e069bb5d791b5bbe89e4068" + "translation": "4b1ce8dde21eb07d470031753f1b2b6da54e3fac" }, "contextMenus.task.calendar.outlook": { "source": "fc2327823b94fa65b207e92718f76e162abe08f4", - "translation": "fc2327823b94fa65b207e92718f76e162abe08f4" + "translation": "62d0a6e6f49b035093b9ea31af83d856595b11dd" }, "contextMenus.task.calendar.yahoo": { "source": "120ca537dd07874066d335e71f9a0df171d8ec28", - "translation": "120ca537dd07874066d335e71f9a0df171d8ec28" + "translation": "4469762aecd4618a37a36bf87e1926c2bc621a0b" }, "contextMenus.task.calendar.downloadIcs": { "source": "8b180d4c410e3d9062bd65d6dbefe626eec813d8", @@ -47956,19 +47956,19 @@ }, "contextMenus.task.calendar.syncToGoogle": { "source": "6bcb0757c7198a7caa3dd59c635a4b5555559938", - "translation": "17592fcc9d088bc00c2a8f9b35fbfefa9d8c4f43" + "translation": "192e6dff8789db430420318cdbfb5398452a0c1a" }, "contextMenus.task.calendar.syncToGoogleNotConfigured": { "source": "f8de13fc800a02f249186b43c628d89f6c69b9d9", - "translation": "1fef647dc605d37882a411faacb5b221b1cb1cf9" + "translation": "f0d4ce9b7eb8b14fff905bff7381b36866d51bc8" }, "contextMenus.task.calendar.syncToGoogleSuccess": { "source": "002e68af1aae01f1c0fdcc551308dbdf4589e124", - "translation": "eb112de5d5e0b74d4abe222166ada6aca49f0f87" + "translation": "9f610776f03e5016dfda3113dd60ef0cfa51f1f2" }, "contextMenus.task.calendar.syncToGoogleFailed": { "source": "cf874861c171987e28af7f10e4174836f724ed27", - "translation": "19237e47f2b9a5b9d02f081023fe4b36e2821eb3" + "translation": "ff19cf0404444a18eabca7e54501155614a09775" }, "contextMenus.task.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -48796,7 +48796,7 @@ }, "ui.taskCard.googleCalendarSyncTooltip": { "source": "9ff5d85645b7b7bcfb1d12fe5c66ecba0487b1ca", - "translation": "64683823336e3c4bb05bdac107099f464c18db66" + "translation": "743936515a4197ea9b712f51e2762bca34eb3ab1" }, "ui.propertyEventCard.unknownFile": { "source": "b05d96bfbcef633c0d11626390d994c13bbf58d0", @@ -49284,7 +49284,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "4963608df7eb2f8ad5791063eecc5066695114e7" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -50210,11 +50210,11 @@ }, "views.basesCalendar.settings.groups.googleCalendars": { "source": "cfa0f6bb3b4422195fc4abb06c02bca780fce33a", - "translation": "9d7627c4afa50c30b37c6e65ea57fe5e0ae04c8c" + "translation": "50729a4aeab3930cb0c7f3d7359169ea139457ba" }, "views.basesCalendar.settings.groups.microsoftCalendars": { "source": "2edac69ff3c043d662037911af60ea54cc1c3746", - "translation": "c16c20ed4e51086b6d670585eaeffa4cf0b5dd96" + "translation": "a44f01dc7e29b987641fd5b74256fc2e825e1808" }, "views.basesCalendar.settings.dateNavigation.navigateToDate": { "source": "2a47407705ef9a77de4778c38853a4122e38b1fd", @@ -50838,7 +50838,7 @@ }, "settings.header.documentation": { "source": "9e9cf3221a30246219863f1d2366e36cb580debc", - "translation": "9e9cf3221a30246219863f1d2366e36cb580debc" + "translation": "c53a5edce43fe80a9022054388e569531bbe9756" }, "settings.header.documentationUrl": { "source": "da82363e11623a8de821a85a3478909ac7ec4fa1", @@ -52582,7 +52582,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "076ddca3551491d5c3ac902745b4982483a246bb" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -53626,7 +53626,7 @@ }, "settings.integrations.autoExport.filePath.placeholder": { "source": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9", - "translation": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9" + "translation": "028ca3d27a511cec04b8048cd401bc9438081000" }, "settings.integrations.autoExport.interval.name": { "source": "556715bd2aecbd24fb83bc1f8cef895487c2d007", @@ -53926,7 +53926,7 @@ }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "3b82fb58a0d336211823acb7a44160121124f8e5" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", @@ -54026,11 +54026,11 @@ }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "1a94add6599c02eb78cae258b9bda1f48ba0d9b4" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "50d25db3b96a4a6e793806e951828a083f3199c4" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -54502,15 +54502,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "ca1c480770ecf414593592eb6441a670c5491962" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "7cf1e5f8543b6c20c115dba775f2142c04b9a505" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "af8dc8a1409628d92f943542761d3b7bb30b4c22" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -54742,7 +54742,7 @@ }, "modals.deviceCode.instructions.intro": { "source": "6f588f0b7cb402c539b8814f65a4798e8da218e0", - "translation": "f70a21a4a66dc520495bf6ab234774aa92abed71" + "translation": "5574215f27cc53b299435107494fab5a4956c5c3" }, "modals.deviceCode.steps.open": { "source": "cf9b77061f7b3126b49d50a6fa68f7ca8c26b7a3", @@ -57554,7 +57554,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "076ddca3551491d5c3ac902745b4982483a246bb" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -59108,7 +59108,7 @@ }, "settings.header.documentation": { "source": "9e9cf3221a30246219863f1d2366e36cb580debc", - "translation": "9e9cf3221a30246219863f1d2366e36cb580debc" + "translation": "1069127253faaccd1c8c27b04bd4dcc97dfe112d" }, "settings.header.documentationUrl": { "source": "da82363e11623a8de821a85a3478909ac7ec4fa1", @@ -59784,7 +59784,7 @@ }, "settings.defaults.bodyTemplate.bodyTemplateFile.placeholder": { "source": "505e5277ec9adf731c105828e0f2939e6bc165fe", - "translation": "505e5277ec9adf731c105828e0f2939e6bc165fe" + "translation": "9b913d31ee96d0e9ef8be4aa4f11cc81bf3be3fd" }, "settings.defaults.bodyTemplate.bodyTemplateFile.ariaLabel": { "source": "2869220f1deea766831798736f09706780b721f4", @@ -60852,7 +60852,7 @@ }, "settings.appearance.taskCards.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "ac29008fe9bd2282f801aee4cb9dd9eca04101bf" }, "settings.appearance.taskCards.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", @@ -61344,7 +61344,7 @@ }, "settings.appearance.projectAutosuggest.requiredPropertyKey.placeholder": { "source": "d0a3e7f81a9885e99049d1cae0336d269d5e47a9", - "translation": "d0a3e7f81a9885e99049d1cae0336d269d5e47a9" + "translation": "e4e46c7235d165987c4994b3bf698841f8ce2e26" }, "settings.appearance.projectAutosuggest.requiredPropertyValue.name": { "source": "41788e0dbabcac0f45a127b9a15c50823fbe911e", @@ -61356,7 +61356,7 @@ }, "settings.appearance.projectAutosuggest.requiredPropertyValue.placeholder": { "source": "98f54143ab4e86b28c3afee0f50f2f51cfb2ed38", - "translation": "98f54143ab4e86b28c3afee0f50f2f51cfb2ed38" + "translation": "22336e6b892f14855e9f71b6ff9d861510dbe57b" }, "settings.appearance.projectAutosuggest.customizeDisplay.name": { "source": "8a7210b95c69b2a52b0ce87041695adfa1ec77e7", @@ -61388,7 +61388,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row1.placeholder": { "source": "398d2038b85f84f7a4c973f0a6682363b6892f12", - "translation": "398d2038b85f84f7a4c973f0a6682363b6892f12" + "translation": "a134d382e1f9cec45f35696b354b42a8f9116297" }, "settings.appearance.projectAutosuggest.displayRows.row2.name": { "source": "67d0756ccdcc47818b53ebc1f5579ac77c550fe2", @@ -61400,7 +61400,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row2.placeholder": { "source": "51b7434e9885414ebb0d1440edc7aca00395e5fb", - "translation": "51b7434e9885414ebb0d1440edc7aca00395e5fb" + "translation": "f0fb127d98e2ea1311dbf9fe98b867c8ace2f18a" }, "settings.appearance.projectAutosuggest.displayRows.row3.name": { "source": "14696a15f0ca131b3e5a75b6496e7bf258410cff", @@ -61412,7 +61412,7 @@ }, "settings.appearance.projectAutosuggest.displayRows.row3.placeholder": { "source": "37885afc8c11c54b8beaae2d2b9106587ee01b47", - "translation": "37885afc8c11c54b8beaae2d2b9106587ee01b47" + "translation": "31778850394d580e56d1a7fc596deacd1f7add89" }, "settings.appearance.projectAutosuggest.quickReference.header": { "source": "c1477a08bbc119a706dea90b93268ceb957f433a", @@ -61624,7 +61624,7 @@ }, "settings.integrations.calendarSubscriptions.defaultNoteTemplate.placeholder": { "source": "407271b9437780b13207e94cd8e15555445fd2c9", - "translation": "407271b9437780b13207e94cd8e15555445fd2c9" + "translation": "ff3378534084e80c94963848e00ce38168f58080" }, "settings.integrations.calendarSubscriptions.defaultNoteFolder.name": { "source": "725e47db9f5f753d6888917e2cf31326086c5e1c", @@ -61636,7 +61636,7 @@ }, "settings.integrations.calendarSubscriptions.defaultNoteFolder.placeholder": { "source": "1c2841e501fd1c8368c9c2fefed7bd1e3d62acc1", - "translation": "1c2841e501fd1c8368c9c2fefed7bd1e3d62acc1" + "translation": "d9de1b428b3d7250172a49a3c3aaa9adc58e4c7d" }, "settings.integrations.calendarSubscriptions.filenameFormat.name": { "source": "52ddeb943a8fdb7b6fbfc5aa22da1f36e38a493c", @@ -61804,7 +61804,7 @@ }, "settings.integrations.subscriptionsList.placeholders.url": { "source": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05", - "translation": "87f1d5a8dd89ab809d235e46cf57acf4211a5b05" + "translation": "cd5067f386a1c7b8b87b2136709f9143dc607d7f" }, "settings.integrations.subscriptionsList.placeholders.filePath": { "source": "5e0150752956631aec10e6d6c2afa36a8c01f6e7", @@ -61812,7 +61812,7 @@ }, "settings.integrations.subscriptionsList.placeholders.localFile": { "source": "519edd5d8a5071ef8699535df1f1530b0ebb328a", - "translation": "519edd5d8a5071ef8699535df1f1530b0ebb328a" + "translation": "d997f0c4dbb0117e4f739a0cdd5ecf7f9c500fb2" }, "settings.integrations.subscriptionsList.statusLabels.enabled": { "source": "df174a3f2faa31814e06540acda7af8825403fac", @@ -61896,7 +61896,7 @@ }, "settings.integrations.autoExport.filePath.placeholder": { "source": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9", - "translation": "ce0e09817cb45784ae67f01c2efe379e8ffa6bd9" + "translation": "40bbf5afceb8150a92c924763ce6f55176844d4d" }, "settings.integrations.autoExport.interval.name": { "source": "556715bd2aecbd24fb83bc1f8cef895487c2d007", @@ -62196,7 +62196,7 @@ }, "settings.integrations.googleCalendarExport.notices.connectionExpired": { "source": "84783064e6e5db03158e0b4e3f3b514b6f9e8027", - "translation": "84783064e6e5db03158e0b4e3f3b514b6f9e8027" + "translation": "8e6407e661df2f63c8d9b77e5538736afcc2a5ea" }, "settings.integrations.googleCalendarExport.notices.syncingTasks": { "source": "14a62327146b2d7ca0b391b34aea8564dbbbd26c", @@ -62292,15 +62292,15 @@ }, "settings.integrations.httpApi.authToken.placeholder": { "source": "dfd8e9adf613d1b2a46dfac755c50174ccecef4c", - "translation": "dfd8e9adf613d1b2a46dfac755c50174ccecef4c" + "translation": "eeac0dc0391a193f4dafd764092a9cdeee037f72" }, "settings.integrations.httpApi.mcp.enable.name": { "source": "1b4cc187a8ef57308b16a947dd993833e1910e12", - "translation": "1b4cc187a8ef57308b16a947dd993833e1910e12" + "translation": "fddf5e0b4570006aa5a94b39dff2db4fc0a40490" }, "settings.integrations.httpApi.mcp.enable.description": { "source": "8d7793dc5646fed97cd0505769894e69227f59c4", - "translation": "8d7793dc5646fed97cd0505769894e69227f59c4" + "translation": "15d1fa9d8f674cb73774b7a3c2f26f64473c0e17" }, "settings.integrations.httpApi.endpoints.header": { "source": "0a8a75f95705a22596bf8a466d95bc6f53a613cf", @@ -62364,7 +62364,7 @@ }, "settings.integrations.webhooks.placeholders.url": { "source": "fa7517b6b6b06cccf039eb795c9e0d9184d37002", - "translation": "fa7517b6b6b06cccf039eb795c9e0d9184d37002" + "translation": "94398e9a1f1a692f2b5eac0cd9611fd6d094651f" }, "settings.integrations.webhooks.placeholders.noEventsSelected": { "source": "73427849b05b6cdda2d7a7de8a8e69ca610d8b04", @@ -62672,7 +62672,7 @@ }, "settings.integrations.webhooks.modals.add.url.name": { "source": "fa7517b6b6b06cccf039eb795c9e0d9184d37002", - "translation": "fa7517b6b6b06cccf039eb795c9e0d9184d37002" + "translation": "94398e9a1f1a692f2b5eac0cd9611fd6d094651f" }, "settings.integrations.webhooks.modals.add.url.description": { "source": "7e97eade845fca964072f4491f0b260000ed8ca6", @@ -62772,15 +62772,15 @@ }, "settings.integrations.mdbaseSpec.learnMore": { "source": "698859ca41e7c55409c85ecc3e624db48e036ee3", - "translation": "698859ca41e7c55409c85ecc3e624db48e036ee3" + "translation": "621b3188c4a250fc071dcdc5eb3c54dbbca2139c" }, "settings.integrations.mdbaseSpec.enable.name": { "source": "4dd758ea2e869c2d28334d1d05ac113c1041488d", - "translation": "4dd758ea2e869c2d28334d1d05ac113c1041488d" + "translation": "9fab51011201f685b326d21c96ceb9c6a245717b" }, "settings.integrations.mdbaseSpec.enable.description": { "source": "5099f3310b8be8de073cdf93685ce2038279c93a", - "translation": "5099f3310b8be8de073cdf93685ce2038279c93a" + "translation": "7acafc42bc2c3414072ea1f228ebb6d56dbb3631" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -63420,7 +63420,7 @@ }, "modals.icsNoteCreation.templatePathPlaceholder": { "source": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0", - "translation": "26f6cf53c168c01cf655bf6f3010f6bc772af4e0" + "translation": "78587692595cffa7e040d861959e37427fac4dc3" }, "modals.unscheduledTasksSelector.title": { "source": "1c97535d510286cde7d544601250ab4df5b52f55", @@ -65824,7 +65824,7 @@ }, "components.propertyVisibilityDropdown.properties.checklistProgress": { "source": "396a57b5107038f90d53659042a82157500053e9", - "translation": "396a57b5107038f90d53659042a82157500053e9" + "translation": "ac29008fe9bd2282f801aee4cb9dd9eca04101bf" }, "components.propertyVisibilityDropdown.properties.recurrence": { "source": "f7ad40f58caa3ee3d9e72be4378d5acf7807f657", diff --git a/src/i18n/resources/de.ts b/src/i18n/resources/de.ts index 7fe96a641..96d5c8df4 100644 --- a/src/i18n/resources/de.ts +++ b/src/i18n/resources/de.ts @@ -204,18 +204,14 @@ export const de: TranslationTree = { customDays: "{count}-Tage-Ansicht", }, settings: { - header: { - documentation: "Documentation", - documentationUrl: "https://tasknotes.dev", - }, groups: { dateNavigation: "Datumsnavigation", events: "Ereignisse", layout: "Layout", propertyBasedEvents: "Eigenschaftsbasierte Ereignisse", calendarSubscriptions: "Kalenderabonnements", - googleCalendars: "Google Kalender", - microsoftCalendars: "Microsoft Kalender", + googleCalendars: "Google-Kalender", + microsoftCalendars: "Microsoft-Kalender", }, dateNavigation: { navigateToDate: "Zum Datum navigieren", @@ -447,7 +443,7 @@ export const de: TranslationTree = { }, settings: { header: { - documentation: "Documentation", + documentation: "Dokumentation", documentationUrl: "https://tasknotes.dev", }, tabs: { @@ -1224,7 +1220,7 @@ export const de: TranslationTree = { scheduled: "Planungsdatum", timeEstimate: "Zeitschätzung", totalTrackedTime: "Gesamte erfasste Zeit", - checklistProgress: "Checklist Progress", + checklistProgress: "Checklistenfortschritt", recurrence: "Wiederholung", completedDate: "Abschlussdatum", createdDate: "Erstellungsdatum", @@ -1639,12 +1635,12 @@ export const de: TranslationTree = { refreshMinutes: "Aktualisierung (Min):", }, typeOptions: { - remote: "Remote URL", + remote: "Remote-URL", local: "Lokale Datei", }, placeholders: { calendarName: "Kalendername", - url: "ICS/iCal URL", + url: "ICS/iCal-URL", filePath: "Lokaler Dateipfad (z.B. Kalender.ics)", localFile: "Kalender.ics", }, @@ -1823,7 +1819,7 @@ export const de: TranslationTree = { noDateToSync: "Aufgabe hat kein geplantes oder Fälligkeitsdatum zum Synchronisieren", syncFailed: "Synchronisierung der Aufgabe mit Google Kalender fehlgeschlagen: {message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", + "Die Google Kalender-Verbindung ist abgelaufen. Bitte verbinde sie unter Einstellungen > Integrationen erneut.", syncingTasks: "Synchronisiere {total} Aufgaben mit Google Kalender...", syncComplete: "Synchronisierung abgeschlossen: {synced} synchronisiert, {failed} fehlgeschlagen, {skipped} übersprungen", @@ -1862,8 +1858,8 @@ export const de: TranslationTree = { }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "MCP-Server aktivieren", + description: "TaskNotes-Werkzeuge über Model Context Protocol am Endpunkt /mcp bereitstellen. Erfordert, dass die HTTP API aktiviert ist.", }, }, endpoints: { @@ -1894,7 +1890,7 @@ export const de: TranslationTree = { transform: "Transformation:", }, placeholders: { - url: "Webhook URL", + url: "Webhook-URL", noEventsSelected: "Keine Events ausgewählt", rawPayload: "Raw-Payload (keine Transformation)", }, @@ -2033,7 +2029,7 @@ export const de: TranslationTree = { transformSection: "Transformationskonfiguration (Optional)", headersSection: "Header-Konfiguration", url: { - name: "Webhook URL", + name: "Webhook-URL", description: "Der Endpunkt, an den Webhook-Payloads gesendet werden", placeholder: "https://dein-service.com/webhook", }, @@ -2075,10 +2071,10 @@ export const de: TranslationTree = { }, mdbaseSpec: { header: "mdbase-Typdefinitionen", - learnMore: "Learn more about mdbase-spec", + learnMore: "Mehr über mdbase-spec erfahren", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "mdbase-Typdefinitionen generieren", + description: "mdbase-Typdateien (mdbase.yaml und _types/task.md) im Stammverzeichnis des Vaults generieren und pflegen, wenn sich deine Einstellungen ändern.", }, }, timeFormats: { @@ -2149,9 +2145,9 @@ export const de: TranslationTree = { }, modals: { deviceCode: { - title: "Google Calendar Autorisierung", + title: "Google Kalender-Autorisierung", instructions: { - intro: "Um Ihren Google Calendar zu verbinden, folgen Sie bitte diesen Schritten:", + intro: "Um Ihren Google Kalender zu verbinden, folgen Sie bitte diesen Schritten:", }, steps: { open: "Öffnen Sie", @@ -2266,7 +2262,7 @@ export const de: TranslationTree = { useTemplateDesc: "Eine Vorlage beim Erstellen des Inhalts anwenden", templatePathLabel: "Vorlagenpfad", templatePathDesc: "Pfad zur Vorlagendatei", - templatePathPlaceholder: "templates/ics-note-template.md", + templatePathPlaceholder: "vorlagen/ics-notiz-vorlage.md", }, unscheduledTasksSelector: { title: "Ungeplante Aufgaben", @@ -2333,7 +2329,7 @@ export const de: TranslationTree = { contextsLabel: "Kontexte", contextsPlaceholder: "kontext1, kontext2", tagsLabel: "Tags", - tagsPlaceholder: "tag1, tag2", + tagsPlaceholder: "schlagwort1, schlagwort2", timeEstimateLabel: "Zeitschätzung (Minuten)", timeEstimatePlaceholder: "30", unsavedChanges: { @@ -3104,7 +3100,7 @@ export const de: TranslationTree = { scheduledDate: "Planungsdatum", timeEstimate: "Zeitschätzung", totalTrackedTime: "Gesamte erfasste Zeit", - checklistProgress: "Checklist Progress", + checklistProgress: "Checklistenfortschritt", recurrence: "Wiederholung", completedDate: "Abschlussdatum", createdDate: "Erstellungsdatum", diff --git a/src/i18n/resources/es.ts b/src/i18n/resources/es.ts index 9752edd04..68f887b98 100644 --- a/src/i18n/resources/es.ts +++ b/src/i18n/resources/es.ts @@ -204,18 +204,14 @@ export const es: TranslationTree = { customDays: "Vista de {count} días", }, settings: { - header: { - documentation: "Documentation", - documentationUrl: "https://tasknotes.dev", - }, groups: { dateNavigation: "Navegación por fecha", events: "Eventos", layout: "Diseño", propertyBasedEvents: "Eventos basados en propiedades", calendarSubscriptions: "Suscripciones de calendario", - googleCalendars: "Google Calendars", - microsoftCalendars: "Microsoft Calendars", + googleCalendars: "Calendarios de Google", + microsoftCalendars: "Calendarios de Microsoft", }, dateNavigation: { navigateToDate: "Navegar a la fecha", @@ -447,7 +443,7 @@ export const es: TranslationTree = { }, settings: { header: { - documentation: "Documentation", + documentation: "Documentación", documentationUrl: "https://tasknotes.dev", }, tabs: { @@ -1224,7 +1220,7 @@ export const es: TranslationTree = { scheduled: "Fecha programada", timeEstimate: "Estimación de tiempo", totalTrackedTime: "Tiempo total rastreado", - checklistProgress: "Checklist Progress", + checklistProgress: "Progreso de la lista de verificación", recurrence: "Recurrencia", completedDate: "Fecha de finalización", createdDate: "Fecha de creación", @@ -1713,19 +1709,19 @@ export const es: TranslationTree = { }, }, googleCalendarExport: { - header: "Exportar tareas a Google Calendar", + header: "Exportar tareas al Calendario de Google", description: - "Sincroniza automáticamente tus tareas a Google Calendar como eventos. Requiere que Google Calendar esté conectado arriba.", + "Sincroniza automáticamente tus tareas al Calendario de Google como eventos. Requiere que el Calendario de Google esté conectado arriba.", enable: { name: "Habilitar exportación de tareas", description: - "Cuando está habilitado, las tareas con fechas se sincronizarán automáticamente a Google Calendar como eventos.", + "Cuando está habilitado, las tareas con fechas se sincronizarán automáticamente al Calendario de Google como eventos.", }, targetCalendar: { name: "Calendario destino", description: "Selecciona en qué calendario crear los eventos de tareas.", placeholder: "Seleccionar un calendario...", - connectFirst: "Conecta Google Calendar primero", + connectFirst: "Conecta el Calendario de Google primero", primarySuffix: " (Principal)", }, syncTrigger: { @@ -1766,7 +1762,7 @@ export const es: TranslationTree = { defaultReminder: { name: "Recordatorio predeterminado", description: - "Agregar un recordatorio emergente a los eventos de Google Calendar. Establece minutos antes del evento (0 = sin recordatorio). Valores comunes: 15, 30, 60, 1440 (1 día).", + "Agregar un recordatorio emergente a los eventos del Calendario de Google. Establece minutos antes del evento (0 = sin recordatorio). Valores comunes: 15, 30, 60, 1440 (1 día).", }, automaticSyncBehavior: { header: "Comportamiento de sincronización automática", @@ -1797,7 +1793,7 @@ export const es: TranslationTree = { syncAllTasks: { name: "Sincronizar todas las tareas", description: - "Sincronizar todas las tareas existentes a Google Calendar. Esto creará eventos para tareas que aún no han sido sincronizadas.", + "Sincronizar todas las tareas existentes al Calendario de Google. Esto creará eventos para tareas que aún no han sido sincronizadas.", buttonText: "Sincronizar todo", }, unlinkAllTasks: { @@ -1812,19 +1808,19 @@ export const es: TranslationTree = { }, notices: { notEnabled: - "La exportación a Google Calendar no está habilitada. Configúrala en Ajustes > Integraciones.", + "La exportación al Calendario de Google no está habilitada. Configúrala en Ajustes > Integraciones.", notEnabledOrConfigured: - "La exportación a Google Calendar no está habilitada o configurada", + "La exportación al Calendario de Google no está habilitada o configurada", serviceNotAvailable: "Servicio de sincronización de calendario no disponible", syncResults: "Sincronizados: {synced}, Fallidos: {failed}, Omitidos: {skipped}", - taskSynced: "Tarea sincronizada a Google Calendar", + taskSynced: "Tarea sincronizada al Calendario de Google", noActiveFile: "No hay archivo actualmente activo", notATask: "El archivo actual no es una tarea", noDateToSync: "La tarea no tiene fecha programada o de vencimiento para sincronizar", - syncFailed: "Error al sincronizar tarea a Google Calendar: {message}", + syncFailed: "Error al sincronizar tarea al Calendario de Google: {message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", - syncingTasks: "Sincronizando {total} tareas a Google Calendar...", + "La conexión con el Calendario de Google ha caducado. Vuelve a conectarla en Configuración > Integraciones.", + syncingTasks: "Sincronizando {total} tareas al Calendario de Google...", syncComplete: "Sincronización completa: {synced} sincronizadas, {failed} fallidas, {skipped} omitidas", eventsDeletedAndUnlinked: "Todos los eventos eliminados y desvinculados", @@ -1862,8 +1858,8 @@ export const es: TranslationTree = { }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "Habilitar servidor MCP", + description: "Expone las herramientas de TaskNotes mediante Model Context Protocol en el endpoint /mcp. Requiere que la API HTTP esté habilitada.", }, }, endpoints: { @@ -2075,10 +2071,10 @@ export const es: TranslationTree = { }, mdbaseSpec: { header: "Definiciones de tipos mdbase", - learnMore: "Learn more about mdbase-spec", + learnMore: "Más información sobre mdbase-spec", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "Generar definiciones de tipos mdbase", + description: "Genera y mantiene archivos de tipos mdbase (mdbase.yaml y _types/task.md) en la raíz de la bóveda cuando cambien tus ajustes.", }, }, timeFormats: { @@ -2144,14 +2140,14 @@ export const es: TranslationTree = { startTimeTrackingWithSelector: "Iniciar seguimiento de tiempo (seleccionar tarea)", editTimeEntries: "Editar entradas de tiempo (seleccionar tarea)", createOrOpenTask: "Crear o abrir tarea", - syncAllTasksGoogleCalendar: "Sincronizar todas las tareas a Google Calendar", - syncCurrentTaskGoogleCalendar: "Sincronizar tarea actual a Google Calendar", + syncAllTasksGoogleCalendar: "Sincronizar todas las tareas al Calendario de Google", + syncCurrentTaskGoogleCalendar: "Sincronizar tarea actual al Calendario de Google", }, modals: { deviceCode: { - title: "Autorización de Google Calendar", + title: "Autorización del Calendario de Google", instructions: { - intro: "Para conectar su Google Calendar, siga estos pasos:", + intro: "Para conectar su Calendario de Google, siga estos pasos:", }, steps: { open: "Abrir", @@ -2266,7 +2262,7 @@ export const es: TranslationTree = { useTemplateDesc: "Aplicar una plantilla al crear el contenido", templatePathLabel: "Ruta de plantilla", templatePathDesc: "Ruta al archivo de plantilla", - templatePathPlaceholder: "templates/ics-note-template.md", + templatePathPlaceholder: "plantillas/plantilla-nota-ics.md", }, unscheduledTasksSelector: { title: "Tareas no programadas", @@ -2664,14 +2660,14 @@ export const es: TranslationTree = { showInExplorer: "Mostrar en explorador de archivos", addToCalendar: "Agregar al calendario", calendar: { - google: "Google Calendar", - outlook: "Outlook Calendar", - yahoo: "Yahoo Calendar", + google: "Calendario de Google", + outlook: "Calendario de Outlook", + yahoo: "Calendario de Yahoo", downloadIcs: "Descargar archivo .ics", - syncToGoogle: "Sincronizar con Google Calendar", - syncToGoogleNotConfigured: "Sincronización con Google Calendar no configurada", - syncToGoogleSuccess: "Tarea sincronizada con Google Calendar", - syncToGoogleFailed: "Error al sincronizar con Google Calendar", + syncToGoogle: "Sincronizar con el Calendario de Google", + syncToGoogleNotConfigured: "Sincronización con el Calendario de Google no configurada", + syncToGoogleSuccess: "Tarea sincronizada con el Calendario de Google", + syncToGoogleFailed: "Error al sincronizar con el Calendario de Google", }, recurrence: "Recurrencia", clearRecurrence: "Limpiar recurrencia", @@ -2961,7 +2957,7 @@ export const es: TranslationTree = { loadingDependencies: "Cargando dependencias...", blockingEmpty: "Sin tareas dependientes", blockingLoadError: "No se pudieron cargar las dependencias", - googleCalendarSyncTooltip: "Sincronizado con Google Calendar", + googleCalendarSyncTooltip: "Sincronizado con el Calendario de Google", }, propertyEventCard: { unknownFile: "Archivo desconocido", @@ -3104,7 +3100,7 @@ export const es: TranslationTree = { scheduledDate: "Fecha programada", timeEstimate: "Estimación de tiempo", totalTrackedTime: "Tiempo total rastreado", - checklistProgress: "Checklist Progress", + checklistProgress: "Progreso de la lista de verificación", recurrence: "Recurrencia", completedDate: "Fecha de finalización", createdDate: "Fecha de creación", diff --git a/src/i18n/resources/fr.ts b/src/i18n/resources/fr.ts index 5aff89edd..04c950fec 100644 --- a/src/i18n/resources/fr.ts +++ b/src/i18n/resources/fr.ts @@ -204,18 +204,14 @@ export const fr: TranslationTree = { customDays: "Vue {count} jours", }, settings: { - header: { - documentation: "Documentation", - documentationUrl: "https://tasknotes.dev", - }, groups: { dateNavigation: "Navigation par date", events: "Événements", layout: "Mise en page", propertyBasedEvents: "Événements basés sur les propriétés", calendarSubscriptions: "Abonnements au calendrier", - googleCalendars: "Google Calendars", - microsoftCalendars: "Microsoft Calendars", + googleCalendars: "Agendas Google", + microsoftCalendars: "Calendriers Microsoft", }, dateNavigation: { navigateToDate: "Naviguer vers la date", @@ -1224,7 +1220,7 @@ export const fr: TranslationTree = { scheduled: "Date planifiée", timeEstimate: "Estimation de temps", totalTrackedTime: "Temps suivi total", - checklistProgress: "Checklist Progress", + checklistProgress: "Progression de la liste de contrôle", recurrence: "Récurrence", completedDate: "Date d'achèvement", createdDate: "Date de création", @@ -1713,19 +1709,19 @@ export const fr: TranslationTree = { }, }, googleCalendarExport: { - header: "Exporter les tâches vers Google Calendar", + header: "Exporter les tâches vers Google Agenda", description: - "Synchronisez automatiquement vos tâches vers Google Calendar en tant qu'événements. Nécessite que Google Calendar soit connecté ci-dessus.", + "Synchronisez automatiquement vos tâches vers Google Agenda en tant qu'événements. Nécessite que Google Agenda soit connecté ci-dessus.", enable: { name: "Activer l'export des tâches", description: - "Lorsqu'activé, les tâches avec des dates seront automatiquement synchronisées vers Google Calendar en tant qu'événements.", + "Lorsqu'activé, les tâches avec des dates seront automatiquement synchronisées vers Google Agenda en tant qu'événements.", }, targetCalendar: { name: "Calendrier cible", description: "Sélectionnez dans quel calendrier créer les événements de tâches.", placeholder: "Sélectionner un calendrier...", - connectFirst: "Connectez d'abord Google Calendar", + connectFirst: "Connectez d'abord Google Agenda", primarySuffix: " (Principal)", }, syncTrigger: { @@ -1766,7 +1762,7 @@ export const fr: TranslationTree = { defaultReminder: { name: "Rappel par défaut", description: - "Ajouter un rappel popup aux événements Google Calendar. Définir les minutes avant l'événement (0 = pas de rappel). Valeurs courantes : 15, 30, 60, 1440 (1 jour).", + "Ajouter un rappel popup aux événements Google Agenda. Définir les minutes avant l'événement (0 = pas de rappel). Valeurs courantes : 15, 30, 60, 1440 (1 jour).", }, automaticSyncBehavior: { header: "Comportement de synchronisation automatique", @@ -1797,7 +1793,7 @@ export const fr: TranslationTree = { syncAllTasks: { name: "Synchroniser toutes les tâches", description: - "Synchroniser toutes les tâches existantes vers Google Calendar. Cela créera des événements pour les tâches qui n'ont pas encore été synchronisées.", + "Synchroniser toutes les tâches existantes vers Google Agenda. Cela créera des événements pour les tâches qui n'ont pas encore été synchronisées.", buttonText: "Tout synchroniser", }, unlinkAllTasks: { @@ -1812,19 +1808,19 @@ export const fr: TranslationTree = { }, notices: { notEnabled: - "L'export Google Calendar n'est pas activé. Configurez-le dans Paramètres > Intégrations.", + "L'export Google Agenda n'est pas activé. Configurez-le dans Paramètres > Intégrations.", notEnabledOrConfigured: - "L'export Google Calendar n'est pas activé ou configuré", + "L'export Google Agenda n'est pas activé ou configuré", serviceNotAvailable: "Service de synchronisation calendrier non disponible", syncResults: "Synchronisés : {synced}, Échoués : {failed}, Ignorés : {skipped}", - taskSynced: "Tâche synchronisée vers Google Calendar", + taskSynced: "Tâche synchronisée vers Google Agenda", noActiveFile: "Aucun fichier n'est actuellement actif", notATask: "Le fichier actuel n'est pas une tâche", noDateToSync: "La tâche n'a pas de date planifiée ou d'échéance à synchroniser", - syncFailed: "Échec de la synchronisation de la tâche vers Google Calendar : {message}", + syncFailed: "Échec de la synchronisation de la tâche vers Google Agenda : {message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", - syncingTasks: "Synchronisation de {total} tâches vers Google Calendar...", + "La connexion à Google Agenda a expiré. Veuillez vous reconnecter dans Paramètres > Intégrations.", + syncingTasks: "Synchronisation de {total} tâches vers Google Agenda...", syncComplete: "Synchronisation terminée : {synced} synchronisées, {failed} échouées, {skipped} ignorées", eventsDeletedAndUnlinked: "Tous les événements supprimés et dissociés", @@ -1862,8 +1858,8 @@ export const fr: TranslationTree = { }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "Activer le serveur MCP", + description: "Expose les outils TaskNotes via le Model Context Protocol sur le point de terminaison /mcp. Nécessite l'activation de l'API HTTP.", }, }, endpoints: { @@ -2075,10 +2071,10 @@ export const fr: TranslationTree = { }, mdbaseSpec: { header: "Définitions de types mdbase", - learnMore: "Learn more about mdbase-spec", + learnMore: "En savoir plus sur mdbase-spec", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "Générer les définitions de types mdbase", + description: "Génère et maintient les fichiers de types mdbase (mdbase.yaml et _types/task.md) à la racine du coffre lorsque vos paramètres changent.", }, }, timeFormats: { @@ -2144,14 +2140,14 @@ export const fr: TranslationTree = { startTimeTrackingWithSelector: "Démarrer le suivi du temps (sélectionner une tâche)", editTimeEntries: "Modifier les entrées de temps (sélectionner une tâche)", createOrOpenTask: "Créer ou ouvrir une tâche", - syncAllTasksGoogleCalendar: "Synchroniser toutes les tâches vers Google Calendar", - syncCurrentTaskGoogleCalendar: "Synchroniser la tâche actuelle vers Google Calendar", + syncAllTasksGoogleCalendar: "Synchroniser toutes les tâches vers Google Agenda", + syncCurrentTaskGoogleCalendar: "Synchroniser la tâche actuelle vers Google Agenda", }, modals: { deviceCode: { - title: "Autorisation Google Calendar", + title: "Autorisation Google Agenda", instructions: { - intro: "Pour connecter votre Google Calendar, veuillez suivre ces étapes :", + intro: "Pour connecter votre Google Agenda, veuillez suivre ces étapes :", }, steps: { open: "Ouvrir", @@ -2266,7 +2262,7 @@ export const fr: TranslationTree = { useTemplateDesc: "Appliquer un modèle lors de la création du contenu", templatePathLabel: "Chemin du modèle", templatePathDesc: "Chemin vers le fichier de modèle", - templatePathPlaceholder: "templates/ics-note-template.md", + templatePathPlaceholder: "modeles/modele-note-ics.md", }, unscheduledTasksSelector: { title: "Tâches non planifiées", @@ -2665,13 +2661,13 @@ export const fr: TranslationTree = { addToCalendar: "Ajouter au calendrier", calendar: { google: "Google Agenda", - outlook: "Outlook Agenda", - yahoo: "Yahoo Agenda", + outlook: "Calendrier Outlook", + yahoo: "Calendrier Yahoo", downloadIcs: "Télécharger le fichier .ics", - syncToGoogle: "Synchroniser avec Google Calendar", - syncToGoogleNotConfigured: "Synchronisation Google Calendar non configurée", - syncToGoogleSuccess: "Tâche synchronisée avec Google Calendar", - syncToGoogleFailed: "Échec de la synchronisation avec Google Calendar", + syncToGoogle: "Synchroniser avec Google Agenda", + syncToGoogleNotConfigured: "Synchronisation Google Agenda non configurée", + syncToGoogleSuccess: "Tâche synchronisée avec Google Agenda", + syncToGoogleFailed: "Échec de la synchronisation avec Google Agenda", }, recurrence: "Récurrence", clearRecurrence: "Effacer la récurrence", @@ -2961,7 +2957,7 @@ export const fr: TranslationTree = { loadingDependencies: "Chargement des dépendances…", blockingEmpty: "Aucune tâche dépendante", blockingLoadError: "Échec du chargement des dépendances", - googleCalendarSyncTooltip: "Synchronisé avec Google Calendar", + googleCalendarSyncTooltip: "Synchronisé avec Google Agenda", }, propertyEventCard: { unknownFile: "Fichier inconnu", @@ -3104,7 +3100,7 @@ export const fr: TranslationTree = { scheduledDate: "Date planifiée", timeEstimate: "Estimation de temps", totalTrackedTime: "Temps suivi total", - checklistProgress: "Checklist Progress", + checklistProgress: "Progression de la liste de contrôle", recurrence: "Récurrence", completedDate: "Date d'achèvement", createdDate: "Date de création", diff --git a/src/i18n/resources/ja.ts b/src/i18n/resources/ja.ts index 5aeb6722f..a4e6184b1 100644 --- a/src/i18n/resources/ja.ts +++ b/src/i18n/resources/ja.ts @@ -204,18 +204,14 @@ export const ja: TranslationTree = { customDays: "{count}日表示", }, settings: { - header: { - documentation: "Documentation", - documentationUrl: "https://tasknotes.dev", - }, groups: { dateNavigation: "日付ナビゲーション", events: "イベント", layout: "レイアウト", propertyBasedEvents: "プロパティベースのイベント", calendarSubscriptions: "カレンダー購読", - googleCalendars: "Google Calendars", - microsoftCalendars: "Microsoft Calendars", + googleCalendars: "Google カレンダー", + microsoftCalendars: "Microsoft カレンダー", }, dateNavigation: { navigateToDate: "日付に移動", @@ -447,7 +443,7 @@ export const ja: TranslationTree = { }, settings: { header: { - documentation: "Documentation", + documentation: "ドキュメント", documentationUrl: "https://tasknotes.dev", }, tabs: { @@ -659,12 +655,12 @@ export const ja: TranslationTree = { defaultContexts: { name: "デフォルトコンテキスト", description: "デフォルトコンテキストのカンマ区切りリスト(例:@home、@work)", - placeholder: "@home, @work", + placeholder: "@自宅, @仕事", }, defaultTags: { name: "デフォルトタグ", description: "デフォルトタグのカンマ区切りリスト(#なし)", - placeholder: "important, urgent", + placeholder: "重要, 緊急", }, defaultProjects: { name: "デフォルトプロジェクト", @@ -744,7 +740,7 @@ export const ja: TranslationTree = { bodyTemplateFile: { name: "ボディテンプレートファイル", description: "タスクボディコンテンツのテンプレートファイルへのパス。{{title}}、{{date}}、{{time}}、{{priority}}、{{status}}などのテンプレート変数をサポート。", - placeholder: "Templates/Task Template.md", + placeholder: "テンプレート/タスクテンプレート.md", ariaLabel: "ボディテンプレートファイルへのパス", }, variablesHeader: "テンプレート変数:", @@ -1077,7 +1073,7 @@ export const ja: TranslationTree = { delayMinutes: "遅延(分):", }, placeholders: { - value: "in-progress", + value: "進行中", label: "進行中", icon: "check, circle, clock", }, @@ -1115,7 +1111,7 @@ export const ja: TranslationTree = { weight: "重み:", }, placeholders: { - value: "high", + value: "高", label: "高優先度", }, weightLabel: "重み:{weight}", @@ -1182,7 +1178,7 @@ export const ja: TranslationTree = { }, placeholders: { displayName: "表示名", - propertyKey: "property-name", + propertyKey: "プロパティ名", defaultValue: "デフォルト値", defaultValueList: "デフォルト値(カンマ区切り)", }, @@ -1195,7 +1191,7 @@ export const ja: TranslationTree = { }, defaultNames: { unnamedField: "名前なしフィールド", - noKey: "no-key", + noKey: "キーなし", }, deleteTooltip: "フィールドを削除", autosuggestFilters: { @@ -1224,7 +1220,7 @@ export const ja: TranslationTree = { scheduled: "予定日", timeEstimate: "時間見積もり", totalTrackedTime: "総追跡時間", - checklistProgress: "Checklist Progress", + checklistProgress: "チェックリストの進捗", recurrence: "繰り返し", completedDate: "完了日", createdDate: "作成日", @@ -1445,22 +1441,22 @@ export const ja: TranslationTree = { requiredTags: { name: "必須タグ", description: "これらのタグのいずれかを持つノートのみを表示(カンマ区切り)。すべてのノートを表示するには空白のままにします。", - placeholder: "project, active, important", + placeholder: "プロジェクト, アクティブ, 重要", }, includeFolders: { name: "含めるフォルダー", description: "これらのフォルダー内のノートのみを表示(カンマ区切りパス)。すべてのフォルダーを表示するには空白のままにします。", - placeholder: "Projects/, Work/Active, Personal", + placeholder: "プロジェクト/, 仕事/アクティブ, 個人", }, requiredPropertyKey: { name: "必須プロパティキー", description: "このフロントマタープロパティが下記の値と一致するノートのみを表示。無視するには空白のままにします。", - placeholder: "type", + placeholder: "タイプ", }, requiredPropertyValue: { name: "必須プロパティ値", description: "プロパティがこの値と等しいノートのみが提案されます。プロパティの存在を要求するには空白のままにします。", - placeholder: "project", + placeholder: "プロジェクト", }, customizeDisplay: { name: "提案表示をカスタマイズ", @@ -1475,17 +1471,17 @@ export const ja: TranslationTree = { row1: { name: "行1", description: "形式:{property|flags}。プロパティ:title、aliases、file.path、file.parent。フラグ:n(Label)はラベルを表示、sは検索可能にします。例:{title|n(Title)|s}", - placeholder: "{title|n(Title)}", + placeholder: "{title|n(タイトル)}", }, row2: { name: "行2(オプション)", description: "一般的なパターン:{aliases|n(Aliases)}、{file.parent|n(Folder)}、literal:カスタムテキスト", - placeholder: "{aliases|n(Aliases)}", + placeholder: "{aliases|n(エイリアス)}", }, row3: { name: "行3(オプション)", description: "{file.path|n(Path)}やカスタムフロントマターフィールドなどの追加情報", - placeholder: "{file.path|n(Path)}", + placeholder: "{file.path|n(パス)}", }, }, quickReference: { @@ -1575,12 +1571,12 @@ export const ja: TranslationTree = { defaultNoteTemplate: { name: "デフォルトノートテンプレート", description: "ICSイベントから作成されるノートのテンプレートファイルへのパス", - placeholder: "Templates/Event Template.md", + placeholder: "テンプレート/イベントテンプレート.md", }, defaultNoteFolder: { name: "デフォルトノートフォルダー", description: "ICSイベントから作成されるノートのフォルダー", - placeholder: "Calendar/Events", + placeholder: "カレンダー/イベント", }, filenameFormat: { name: "ICSノートファイル名形式", @@ -1644,9 +1640,9 @@ export const ja: TranslationTree = { }, placeholders: { calendarName: "カレンダー名", - url: "ICS/iCal URL", + url: "ICS/iCal のURL", filePath: "ローカルファイルパス(例:Calendar.ics)", - localFile: "Calendar.ics", + localFile: "カレンダー.ics", }, statusLabels: { enabled: "有効", @@ -1679,7 +1675,7 @@ export const ja: TranslationTree = { filePath: { name: "エクスポートファイルパス", description: "ICSファイルを保存するパス(ボルトルートからの相対パス)", - placeholder: "tasknotes-calendar.ics", + placeholder: "tasknotes-カレンダー.ics", }, interval: { name: "更新間隔(5から1440分の間)", @@ -1823,7 +1819,7 @@ export const ja: TranslationTree = { noDateToSync: "タスクに同期する予定日または期限がありません", syncFailed: "タスクのGoogleカレンダーへの同期に失敗しました:{message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", + "Google カレンダーの接続が期限切れです。設定 > 統合で再接続してください。", syncingTasks: "{total}件のタスクをGoogleカレンダーに同期中...", syncComplete: "同期完了:{synced}件同期、{failed}件失敗、{skipped}件スキップ", @@ -1858,12 +1854,12 @@ export const ja: TranslationTree = { authToken: { name: "API認証トークン", description: "API認証に必要なトークン(認証なしの場合は空白のままにする)", - placeholder: "your-secret-token", + placeholder: "あなたのシークレットトークン", }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "MCP サーバーを有効にする", + description: "Model Context Protocol を介して /mcp エンドポイントで TaskNotes ツールを公開します。HTTP API を有効にする必要があります。", }, }, endpoints: { @@ -1894,7 +1890,7 @@ export const ja: TranslationTree = { transform: "変換:", }, placeholders: { - url: "Webhook URL", + url: "Webhook のURL", noEventsSelected: "イベントが選択されていません", rawPayload: "Rawペイロード(変換なし)", }, @@ -2033,7 +2029,7 @@ export const ja: TranslationTree = { transformSection: "変換設定(オプション)", headersSection: "ヘッダー設定", url: { - name: "Webhook URL", + name: "Webhook のURL", description: "Webhookペイロードが送信されるエンドポイント", placeholder: "https://your-service.com/webhook", }, @@ -2075,10 +2071,10 @@ export const ja: TranslationTree = { }, mdbaseSpec: { header: "mdbase型定義", - learnMore: "Learn more about mdbase-spec", + learnMore: "mdbase-spec について詳しく見る", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "mdbase 型定義を生成", + description: "設定の変更に合わせて、ボルトルートに mdbase 型ファイル(mdbase.yaml と _types/task.md)を生成して維持します。", }, }, timeFormats: { @@ -2266,7 +2262,7 @@ export const ja: TranslationTree = { useTemplateDesc: "コンテンツ作成時にテンプレートを適用", templatePathLabel: "テンプレートパス", templatePathDesc: "テンプレートファイルへのパス", - templatePathPlaceholder: "templates/ics-note-template.md", + templatePathPlaceholder: "テンプレート/ICSノートテンプレート.md", }, unscheduledTasksSelector: { title: "予定されていないタスク", @@ -2331,9 +2327,9 @@ export const ja: TranslationTree = { projectsTooltip: "ファジー検索を使用してプロジェクトノートを選択", projectsRemoveTooltip: "プロジェクトを削除", contextsLabel: "コンテキスト", - contextsPlaceholder: "context1, context2", + contextsPlaceholder: "コンテキスト1, コンテキスト2", tagsLabel: "タグ", - tagsPlaceholder: "tag1, tag2", + tagsPlaceholder: "タグ1, タグ2", timeEstimateLabel: "時間見積もり(分)", timeEstimatePlaceholder: "30", unsavedChanges: { @@ -2395,7 +2391,7 @@ export const ja: TranslationTree = { textPlaceholder: "{field}を入力...", numberPlaceholder: "0", datePlaceholder: "YYYY-MM-DD", - listPlaceholder: "item1, item2, item3", + listPlaceholder: "項目1, 項目2, 項目3", pickDate: "{field}日付を選択", }, recurrence: { @@ -3104,7 +3100,7 @@ export const ja: TranslationTree = { scheduledDate: "予定日", timeEstimate: "時間見積もり", totalTrackedTime: "総追跡時間", - checklistProgress: "Checklist Progress", + checklistProgress: "チェックリストの進捗", recurrence: "繰り返し", completedDate: "完了日", createdDate: "作成日", diff --git a/src/i18n/resources/ko.ts b/src/i18n/resources/ko.ts index 68d50fbd1..2a700945f 100644 --- a/src/i18n/resources/ko.ts +++ b/src/i18n/resources/ko.ts @@ -655,12 +655,12 @@ export const ko: TranslationTree = { defaultContexts: { name: "기본 컨텍스트", description: "쉼표로 구분된 기본 컨텍스트 목록 (예: @home, @work)", - placeholder: "@home, @work", + placeholder: "@집, @직장", }, defaultTags: { name: "기본 태그", description: "# 없이 쉼표로 구분된 기본 태그 목록", - placeholder: "important, urgent", + placeholder: "중요, 긴급", }, defaultProjects: { name: "기본 프로젝트", @@ -740,7 +740,7 @@ export const ko: TranslationTree = { bodyTemplateFile: { name: "본문 템플릿 파일", description: "작업 본문 콘텐츠용 템플릿 파일 경로. {{title}}, {{date}}, {{time}}, {{priority}}, {{status}} 등의 템플릿 변수를 지원합니다.", - placeholder: "Templates/Task Template.md", + placeholder: "템플릿/작업 템플릿.md", ariaLabel: "본문 템플릿 파일 경로", }, variablesHeader: "템플릿 변수:", @@ -1046,7 +1046,7 @@ export const ko: TranslationTree = { delayMinutes: "지연 (분):", }, placeholders: { - value: "in-progress", + value: "진행중", label: "진행 중", icon: "check, circle, clock", }, @@ -1078,7 +1078,7 @@ export const ko: TranslationTree = { color: "색상:", }, placeholders: { - value: "high", + value: "높음", label: "높은 우선순위", }, deleteConfirm: "최소 하나의 우선순위가 있어야 합니다", @@ -1144,7 +1144,7 @@ export const ko: TranslationTree = { }, placeholders: { displayName: "표시 이름", - propertyKey: "property-name", + propertyKey: "속성-이름", defaultValue: "기본값", defaultValueList: "기본값 (쉼표로 구분)", }, @@ -1157,7 +1157,7 @@ export const ko: TranslationTree = { }, defaultNames: { unnamedField: "이름 없는 필드", - noKey: "no-key", + noKey: "키-없음", }, deleteTooltip: "필드 삭제", autosuggestFilters: { @@ -1186,7 +1186,7 @@ export const ko: TranslationTree = { scheduled: "예정일", timeEstimate: "시간 예상", totalTrackedTime: "총 기록 시간", - checklistProgress: "Checklist Progress", + checklistProgress: "체크리스트 진행률", recurrence: "반복", completedDate: "완료일", createdDate: "생성일", @@ -1395,22 +1395,22 @@ export const ko: TranslationTree = { requiredTags: { name: "필수 태그", description: "이러한 태그 중 하나가 있는 노트만 표시 (쉼표로 구분). 모든 노트를 표시하려면 비워두세요.", - placeholder: "project, active, important", + placeholder: "프로젝트, 활성, 중요", }, includeFolders: { name: "포함 폴더", description: "이러한 폴더의 노트만 표시 (쉼표로 구분된 경로). 모든 폴더를 표시하려면 비워두세요.", - placeholder: "Projects/, Work/Active, Personal", + placeholder: "프로젝트/, 업무/활성, 개인", }, requiredPropertyKey: { name: "필수 속성 키", description: "이 프론트매터 속성이 아래 값과 일치하는 노트만 표시. 무시하려면 비워두세요.", - placeholder: "type", + placeholder: "유형", }, requiredPropertyValue: { name: "필수 속성 값", description: "속성이 이 값과 같은 노트만 제안됩니다. 속성 존재만 요구하려면 비워두세요.", - placeholder: "project", + placeholder: "프로젝트", }, customizeDisplay: { name: "제안 표시 사용자 지정", @@ -1425,17 +1425,17 @@ export const ko: TranslationTree = { row1: { name: "행 1", description: "형식: {property|flags}. 속성: title, aliases, file.path, file.parent. 플래그: n(Label)은 레이블 표시, s는 검색 가능. 예: {title|n(Title)|s}", - placeholder: "{title|n(Title)}", + placeholder: "{title|n(제목)}", }, row2: { name: "행 2 (선택사항)", description: "일반적인 패턴: {aliases|n(Aliases)}, {file.parent|n(Folder)}, literal:Custom Text", - placeholder: "{aliases|n(Aliases)}", + placeholder: "{aliases|n(별칭)}", }, row3: { name: "행 3 (선택사항)", description: "{file.path|n(Path)} 또는 사용자 지정 프론트매터 필드 같은 추가 정보", - placeholder: "{file.path|n(Path)}", + placeholder: "{file.path|n(경로)}", }, }, quickReference: { @@ -1525,12 +1525,12 @@ export const ko: TranslationTree = { defaultNoteTemplate: { name: "기본 노트 템플릿", description: "ICS 이벤트에서 생성된 노트용 템플릿 파일 경로", - placeholder: "Templates/Event Template.md", + placeholder: "템플릿/이벤트 템플릿.md", }, defaultNoteFolder: { name: "기본 노트 폴더", description: "ICS 이벤트에서 생성된 노트용 폴더", - placeholder: "Calendar/Events", + placeholder: "캘린더/이벤트", }, filenameFormat: { name: "ICS 노트 파일명 형식", @@ -1594,9 +1594,9 @@ export const ko: TranslationTree = { }, placeholders: { calendarName: "캘린더 이름", - url: "ICS/iCal URL", + url: "ICS/iCal 주소", filePath: "로컬 파일 경로 (예: Calendar.ics)", - localFile: "Calendar.ics", + localFile: "캘린더.ics", }, statusLabels: { enabled: "활성화됨", @@ -1629,7 +1629,7 @@ export const ko: TranslationTree = { filePath: { name: "내보내기 파일 경로", description: "ICS 파일이 저장될 경로 (보관소 루트 기준)", - placeholder: "tasknotes-calendar.ics", + placeholder: "tasknotes-캘린더.ics", }, interval: { name: "업데이트 간격 (5~1440분)", @@ -1773,7 +1773,7 @@ export const ko: TranslationTree = { noDateToSync: "동기화할 예정 날짜 또는 마감 날짜가 없습니다", syncFailed: "Google 캘린더에 작업 동기화 실패: {message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", + "Google 캘린더 연결이 만료되었습니다. 설정 > 통합에서 다시 연결하세요.", syncingTasks: "{total}개의 작업을 Google 캘린더에 동기화 중...", syncComplete: "동기화 완료: {synced}개 동기화됨, {failed}개 실패, {skipped}개 건너뜀", @@ -1808,12 +1808,12 @@ export const ko: TranslationTree = { authToken: { name: "API 인증 토큰", description: "API 인증에 필요한 토큰 (인증 없이 사용하려면 비워두세요)", - placeholder: "your-secret-token", + placeholder: "비밀-토큰", }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "MCP 서버 활성화", + description: "Model Context Protocol을 통해 /mcp 엔드포인트에서 TaskNotes 도구를 노출합니다. HTTP API가 활성화되어 있어야 합니다.", }, }, endpoints: { @@ -2025,10 +2025,10 @@ export const ko: TranslationTree = { }, mdbaseSpec: { header: "mdbase 타입 정의", - learnMore: "Learn more about mdbase-spec", + learnMore: "mdbase-spec에 대해 자세히 알아보기", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "mdbase 타입 정의 생성", + description: "설정이 변경될 때 보관소 루트에 mdbase 타입 파일(mdbase.yaml 및 _types/task.md)을 생성하고 유지합니다.", }, }, timeFormats: { @@ -2206,7 +2206,7 @@ export const ko: TranslationTree = { titleDesc: "새 콘텐츠의 제목", folderLabel: "폴더", folderDesc: "대상 폴더 (보관소 루트를 사용하려면 비워두세요)", - folderPlaceholder: "folder/subfolder", + folderPlaceholder: "폴더/하위폴더", createButton: "생성", startLabel: "시작: ", endLabel: "종료: ", @@ -2216,7 +2216,7 @@ export const ko: TranslationTree = { useTemplateDesc: "콘텐츠 생성 시 템플릿 적용", templatePathLabel: "템플릿 경로", templatePathDesc: "템플릿 파일 경로", - templatePathPlaceholder: "templates/ics-note-template.md", + templatePathPlaceholder: "템플릿/ICS 노트 템플릿.md", }, unscheduledTasksSelector: { title: "예정되지 않은 작업", @@ -2281,9 +2281,9 @@ export const ko: TranslationTree = { projectsTooltip: "퍼지 검색을 사용하여 프로젝트 노트 선택", projectsRemoveTooltip: "프로젝트 제거", contextsLabel: "컨텍스트", - contextsPlaceholder: "context1, context2", + contextsPlaceholder: "컨텍스트1, 컨텍스트2", tagsLabel: "태그", - tagsPlaceholder: "tag1, tag2", + tagsPlaceholder: "태그1, 태그2", timeEstimateLabel: "시간 예상 (분)", timeEstimatePlaceholder: "30", unsavedChanges: { @@ -3054,7 +3054,7 @@ export const ko: TranslationTree = { scheduledDate: "예정일", timeEstimate: "시간 예상", totalTrackedTime: "총 기록 시간", - checklistProgress: "Checklist Progress", + checklistProgress: "체크리스트 진행률", recurrence: "반복", completedDate: "완료일", createdDate: "생성일", diff --git a/src/i18n/resources/pt.ts b/src/i18n/resources/pt.ts index bfefc6676..27bf6e0e6 100644 --- a/src/i18n/resources/pt.ts +++ b/src/i18n/resources/pt.ts @@ -204,18 +204,14 @@ export const pt: TranslationTree = { customDays: "Visualização de {count} dias" }, settings: { - header: { - documentation: "Documentation", - documentationUrl: "https://tasknotes.dev", - }, groups: { dateNavigation: "Navegação de Data", events: "Eventos", layout: "Layout", propertyBasedEvents: "Eventos baseados em propriedade", calendarSubscriptions: "Inscrições de calendário", - googleCalendars: "Google Calendars", - microsoftCalendars: "Microsoft Calendars" + googleCalendars: "Calendários do Google", + microsoftCalendars: "Calendários da Microsoft" }, dateNavigation: { navigateToDate: "Navegar para data", @@ -447,7 +443,7 @@ export const pt: TranslationTree = { }, settings: { header: { - documentation: "Documentation", + documentation: "Documentação", documentationUrl: "https://tasknotes.dev", }, tabs: { @@ -962,17 +958,17 @@ export const pt: TranslationTree = { dateCreated: { name: "Data de Criação", description: - "Timestamp de quando a tarefa foi criada. Definido automaticamente e usado para ordenação por ordem de criação.", + "Carimbo de data/hora de quando a tarefa foi criada. Definido automaticamente e usado para ordenação por ordem de criação.", }, dateModified: { name: "Data de Modificação", description: - "Timestamp da última alteração na tarefa. Atualizado automaticamente quando qualquer propriedade da tarefa muda.", + "Carimbo de data/hora da última alteração na tarefa. Atualizado automaticamente quando qualquer propriedade da tarefa muda.", }, completedDate: { name: "Data de Conclusão", description: - "Timestamp de quando a tarefa foi marcada como concluída. Definido automaticamente quando o status muda para um estado concluído.", + "Carimbo de data/hora de quando a tarefa foi marcada como concluída. Definido automaticamente quando o status muda para um estado concluído.", }, archiveTag: { name: "Tag de Arquivo", @@ -982,7 +978,7 @@ export const pt: TranslationTree = { timeEntries: { name: "Entradas de Tempo", description: - "Registros de sessões de rastreamento de tempo para esta tarefa. Cada entrada armazena timestamps de início e fim. Usado para calcular o tempo total gasto.", + "Registros de sessões de rastreamento de tempo para esta tarefa. Cada entrada armazena carimbos de data/hora de início e fim. Usado para calcular o tempo total gasto.", }, completeInstances: { name: "Instâncias Concluídas", @@ -1227,7 +1223,7 @@ export const pt: TranslationTree = { scheduled: "Data Agendada", timeEstimate: "Estimativa de Tempo", totalTrackedTime: "Tempo Total Registrado", - checklistProgress: "Checklist Progress", + checklistProgress: "Progresso da lista de verificação", recurrence: "Recorrência", completedDate: "Data de Conclusão", createdDate: "Data de Criação", @@ -1252,7 +1248,7 @@ export const pt: TranslationTree = { options: { title: "Título da tarefa (Não atualiza)", zettel: "Formato Zettelkasten (AAMMDD + segundos base36 desde a meia-noite)", - timestamp: "Timestamp completo (AAAA-MM-DD-HHMMSS)", + timestamp: "Carimbo de data/hora completo (AAAA-MM-DD-HHMMSS)", custom: "Modelo personalizado" } }, @@ -1591,7 +1587,7 @@ export const pt: TranslationTree = { options: { title: "Título do evento", zettel: "Formato Zettelkasten", - timestamp: "Timestamp", + timestamp: "Carimbo de data/hora", custom: "Modelo personalizado" } }, @@ -1682,7 +1678,7 @@ export const pt: TranslationTree = { filePath: { name: "Caminho do arquivo de exportação", description: "Caminho onde o arquivo ICS será salvo (relativo à raiz do cofre)", - placeholder: "tasknotes-calendar.ics" + placeholder: "tasknotes-calendario.ics" }, interval: { name: "Intervalo de atualização (entre 5 e 1440 minutos)", @@ -1716,19 +1712,19 @@ export const pt: TranslationTree = { } }, googleCalendarExport: { - header: "Exportar tarefas para o Google Calendar", + header: "Exportar tarefas para o Google Agenda", description: - "Sincronize automaticamente suas tarefas para o Google Calendar como eventos. Requer que o Google Calendar esteja conectado acima.", + "Sincronize automaticamente suas tarefas para o Google Agenda como eventos. Requer que o Google Agenda esteja conectado acima.", enable: { name: "Ativar exportação de tarefas", description: - "Quando ativado, tarefas com datas serão automaticamente sincronizadas para o Google Calendar como eventos." + "Quando ativado, tarefas com datas serão automaticamente sincronizadas para o Google Agenda como eventos." }, targetCalendar: { name: "Calendário de destino", description: "Selecione em qual calendário criar eventos de tarefas.", placeholder: "Selecionar um calendário...", - connectFirst: "Conecte o Google Calendar primeiro", + connectFirst: "Conecte o Google Agenda primeiro", primarySuffix: " (Principal)" }, syncTrigger: { @@ -1769,7 +1765,7 @@ export const pt: TranslationTree = { defaultReminder: { name: "Lembrete padrão", description: - "Adicionar um lembrete popup aos eventos do Google Calendar. Defina minutos antes do evento (0 = sem lembrete). Valores comuns: 15, 30, 60, 1440 (1 dia)." + "Adicionar um lembrete popup aos eventos do Google Agenda. Defina minutos antes do evento (0 = sem lembrete). Valores comuns: 15, 30, 60, 1440 (1 dia)." }, automaticSyncBehavior: { header: "Comportamento de sincronização automática" @@ -1800,7 +1796,7 @@ export const pt: TranslationTree = { syncAllTasks: { name: "Sincronizar todas as tarefas", description: - "Sincronizar todas as tarefas existentes para o Google Calendar. Isso criará eventos para tarefas que ainda não foram sincronizadas.", + "Sincronizar todas as tarefas existentes para o Google Agenda. Isso criará eventos para tarefas que ainda não foram sincronizadas.", buttonText: "Sincronizar tudo" }, unlinkAllTasks: { @@ -1815,19 +1811,19 @@ export const pt: TranslationTree = { }, notices: { notEnabled: - "Exportação para o Google Calendar não está ativada. Configure em Configurações > Integrações.", + "Exportação para o Google Agenda não está ativada. Configure em Configurações > Integrações.", notEnabledOrConfigured: - "Exportação para o Google Calendar não está ativada ou configurada", + "Exportação para o Google Agenda não está ativada ou configurada", serviceNotAvailable: "Serviço de sincronização de calendário não disponível", syncResults: "Sincronizados: {synced}, Falharam: {failed}, Ignorados: {skipped}", - taskSynced: "Tarefa sincronizada para o Google Calendar", + taskSynced: "Tarefa sincronizada para o Google Agenda", noActiveFile: "Nenhum arquivo está atualmente ativo", notATask: "O arquivo atual não é uma tarefa", noDateToSync: "Tarefa não tem data agendada ou de vencimento para sincronizar", - syncFailed: "Falha ao sincronizar tarefa para o Google Calendar: {message}", + syncFailed: "Falha ao sincronizar tarefa para o Google Agenda: {message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", - syncingTasks: "Sincronizando {total} tarefas para o Google Calendar...", + "A conexão com o Google Agenda expirou. Reconecte em Configurações > Integrações.", + syncingTasks: "Sincronizando {total} tarefas para o Google Agenda...", syncComplete: "Sincronização completa: {synced} sincronizadas, {failed} falharam, {skipped} ignoradas", eventsDeletedAndUnlinked: "Todos os eventos excluídos e desvinculados", @@ -1865,8 +1861,8 @@ export const pt: TranslationTree = { }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "Ativar servidor MCP", + description: "Exponha as ferramentas do TaskNotes via Model Context Protocol no endpoint /mcp. Requer que a API HTTP esteja ativada.", }, }, endpoints: { @@ -2078,10 +2074,10 @@ export const pt: TranslationTree = { }, mdbaseSpec: { header: "Definições de tipos mdbase", - learnMore: "Learn more about mdbase-spec", + learnMore: "Saiba mais sobre mdbase-spec", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "Gerar definições de tipos mdbase", + description: "Gere e mantenha arquivos de tipos mdbase (mdbase.yaml e _types/task.md) na raiz do cofre conforme suas configurações mudam.", }, }, timeFormats: { @@ -2152,14 +2148,14 @@ export const pt: TranslationTree = { startTimeTrackingWithSelector: "Iniciar registro de tempo (selecionar tarefa)", editTimeEntries: "Editar registros de tempo (selecionar tarefa)", createOrOpenTask: "Criar ou abrir tarefa", - syncAllTasksGoogleCalendar: "Sincronizar todas as tarefas para o Google Calendar", - syncCurrentTaskGoogleCalendar: "Sincronizar tarefa atual para o Google Calendar" + syncAllTasksGoogleCalendar: "Sincronizar todas as tarefas para o Google Agenda", + syncCurrentTaskGoogleCalendar: "Sincronizar tarefa atual para o Google Agenda" }, modals: { deviceCode: { - title: "Autorização do Google Calendar", + title: "Autorização do Google Agenda", instructions: { - intro: "Para conectar seu Google Calendar, por favor, siga estes passos:" + intro: "Para conectar seu Google Agenda, por favor, siga estes passos:" }, steps: { open: "Abra", @@ -2341,7 +2337,7 @@ export const pt: TranslationTree = { contextsLabel: "Contextos", contextsPlaceholder: "contexto1, contexto2", tagsLabel: "Tags", - tagsPlaceholder: "tag1, tag2", + tagsPlaceholder: "etiqueta1, etiqueta2", timeEstimateLabel: "Estimativa de tempo (minutos)", timeEstimatePlaceholder: "30", unsavedChanges: { @@ -2403,7 +2399,7 @@ export const pt: TranslationTree = { textPlaceholder: "Digite {field}...", numberPlaceholder: "0", datePlaceholder: "AAAA-MM-DD", - listPlaceholder: "item1, item2, item3", + listPlaceholder: "item 1, item 2, item 3", pickDate: "Escolher data {field}" }, recurrence: { @@ -2672,14 +2668,14 @@ export const pt: TranslationTree = { showInExplorer: "Mostrar no explorador de arquivos", addToCalendar: "Adicionar ao calendário", calendar: { - google: "Google Calendar", - outlook: "Outlook Calendar", - yahoo: "Yahoo Calendar", + google: "Google Agenda", + outlook: "Calendário do Outlook", + yahoo: "Calendário do Yahoo", downloadIcs: "Baixar arquivo .ics", - syncToGoogle: "Sincronizar com o Google Calendar", - syncToGoogleNotConfigured: "Sincronização com Google Calendar não configurada", - syncToGoogleSuccess: "Tarefa sincronizada com o Google Calendar", - syncToGoogleFailed: "Falha ao sincronizar com o Google Calendar" + syncToGoogle: "Sincronizar com o Google Agenda", + syncToGoogleNotConfigured: "Sincronização com Google Agenda não configurada", + syncToGoogleSuccess: "Tarefa sincronizada com o Google Agenda", + syncToGoogleFailed: "Falha ao sincronizar com o Google Agenda" }, recurrence: "Recorrência", clearRecurrence: "Limpar recorrência", @@ -2975,7 +2971,7 @@ export const pt: TranslationTree = { loadingDependencies: "Carregando dependências...", blockingEmpty: "Nenhuma tarefa dependente", blockingLoadError: "Falha ao carregar dependências", - googleCalendarSyncTooltip: "Sincronizado com o Google Calendar" + googleCalendarSyncTooltip: "Sincronizado com o Google Agenda" }, propertyEventCard: { unknownFile: "Arquivo desconhecido" @@ -3118,7 +3114,7 @@ export const pt: TranslationTree = { scheduledDate: "Data Agendada", timeEstimate: "Estimativa de Tempo", totalTrackedTime: "Tempo Total Registrado", - checklistProgress: "Checklist Progress", + checklistProgress: "Progresso da lista de verificação", recurrence: "Recorrência", completedDate: "Data de Conclusão", createdDate: "Data de Criação", diff --git a/src/i18n/resources/ru.ts b/src/i18n/resources/ru.ts index d14995dca..cae0ff17c 100644 --- a/src/i18n/resources/ru.ts +++ b/src/i18n/resources/ru.ts @@ -204,18 +204,14 @@ export const ru: TranslationTree = { customDays: "Вид на {count} дней", }, settings: { - header: { - documentation: "Documentation", - documentationUrl: "https://tasknotes.dev", - }, groups: { dateNavigation: "Навигация по датам", events: "События", layout: "Макет", propertyBasedEvents: "События на основе свойств", calendarSubscriptions: "Подписки календаря", - googleCalendars: "Google Календари", - microsoftCalendars: "Microsoft Календари", + googleCalendars: "Календари Google", + microsoftCalendars: "Календари Microsoft", }, dateNavigation: { navigateToDate: "Перейти к дате", @@ -447,7 +443,7 @@ export const ru: TranslationTree = { }, settings: { header: { - documentation: "Documentation", + documentation: "Документация", documentationUrl: "https://tasknotes.dev", }, tabs: { @@ -1224,7 +1220,7 @@ export const ru: TranslationTree = { scheduled: "Запланированная дата", timeEstimate: "Оценка времени", totalTrackedTime: "Общее отслеженное время", - checklistProgress: "Checklist Progress", + checklistProgress: "Прогресс чек-листа", recurrence: "Повторение", completedDate: "Дата завершения", createdDate: "Дата создания", @@ -1679,7 +1675,7 @@ export const ru: TranslationTree = { filePath: { name: "Путь к файлу экспорта", description: "Путь, где будет сохранен файл ICS (относительно корня хранилища)", - placeholder: "tasknotes-calendar.ics", + placeholder: "tasknotes-kalendar.ics", }, interval: { name: "Интервал обновления (между 5 и 1440 минут)", @@ -1823,7 +1819,7 @@ export const ru: TranslationTree = { noDateToSync: "У задачи нет запланированной даты или срока для синхронизации", syncFailed: "Не удалось синхронизировать задачу с Google Календарём: {message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", + "Срок действия подключения к Google Календарю истёк. Подключитесь заново в разделе Настройки > Интеграции.", syncingTasks: "Синхронизация {total} задач с Google Календарём...", syncComplete: "Синхронизация завершена: {synced} синхронизировано, {failed} ошибок, {skipped} пропущено", @@ -1862,8 +1858,8 @@ export const ru: TranslationTree = { }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "Включить сервер MCP", + description: "Предоставляет инструменты TaskNotes через Model Context Protocol на конечной точке /mcp. Требуется включённый HTTP API.", }, }, endpoints: { @@ -2075,10 +2071,10 @@ export const ru: TranslationTree = { }, mdbaseSpec: { header: "Определения типов mdbase", - learnMore: "Learn more about mdbase-spec", + learnMore: "Подробнее о mdbase-spec", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "Создавать определения типов mdbase", + description: "Создаёт и поддерживает файлы типов mdbase (mdbase.yaml и _types/task.md) в корне хранилища при изменении настроек.", }, }, timeFormats: { @@ -2151,7 +2147,7 @@ export const ru: TranslationTree = { deviceCode: { title: "Авторизация Google Календаря", instructions: { - intro: "До connect your Google Calendar, please follow these steps:", + intro: "Чтобы подключить Google Календарь, выполните следующие шаги:", }, steps: { open: "Открыть", @@ -3104,7 +3100,7 @@ export const ru: TranslationTree = { scheduledDate: "Запланированная дата", timeEstimate: "Оценка времени", totalTrackedTime: "Общее отслеженное время", - checklistProgress: "Checklist Progress", + checklistProgress: "Прогресс чек-листа", recurrence: "Повторение", completedDate: "Дата завершения", createdDate: "Дата создания", diff --git a/src/i18n/resources/zh.ts b/src/i18n/resources/zh.ts index 38ecb86e3..a53ee31cf 100644 --- a/src/i18n/resources/zh.ts +++ b/src/i18n/resources/zh.ts @@ -204,10 +204,6 @@ export const zh: TranslationTree = { customDays: "{count}天视图", }, settings: { - header: { - documentation: "Documentation", - documentationUrl: "https://tasknotes.dev", - }, groups: { dateNavigation: "日期导航", events: "事件", @@ -447,7 +443,7 @@ export const zh: TranslationTree = { }, settings: { header: { - documentation: "Documentation", + documentation: "文档", documentationUrl: "https://tasknotes.dev", }, tabs: { @@ -743,7 +739,7 @@ export const zh: TranslationTree = { bodyTemplateFile: { name: "正文模板文件", description: "任务正文内容的模板文件路径。支持模板变量如{{title}}、{{date}}、{{time}}、{{priority}}、{{status}}等。", - placeholder: "Templates/Task Template.md", + placeholder: "模板/任务模板.md", ariaLabel: "正文模板文件路径", }, variablesHeader: "模板变量:", @@ -1223,7 +1219,7 @@ export const zh: TranslationTree = { scheduled: "安排日期", timeEstimate: "时间估计", totalTrackedTime: "总跟踪时间", - checklistProgress: "Checklist Progress", + checklistProgress: "清单进度", recurrence: "重复", completedDate: "完成日期", createdDate: "创建日期", @@ -1454,12 +1450,12 @@ export const zh: TranslationTree = { requiredPropertyKey: { name: "必需属性键", description: "仅显示此前置属性与下面值匹配的笔记。留空以忽略。", - placeholder: "type", + placeholder: "类型", }, requiredPropertyValue: { name: "必需属性值", description: "仅建议属性等于此值的笔记。留空以要求属性存在。", - placeholder: "project", + placeholder: "项目", }, customizeDisplay: { name: "自定义建议显示", @@ -1474,17 +1470,17 @@ export const zh: TranslationTree = { row1: { name: "第1行", description: "格式:{property|flags}。属性:title、aliases、file.path、file.parent。标志:n(Label)显示标签,s使其可搜索。示例:{title|n(Title)|s}", - placeholder: "{title|n(Title)}", + placeholder: "{title|n(标题)}", }, row2: { name: "第2行(可选)", description: "常见模式:{aliases|n(Aliases)}、{file.parent|n(Folder)}、literal:自定义文本", - placeholder: "{aliases|n(Aliases)}", + placeholder: "{aliases|n(别名)}", }, row3: { name: "第3行(可选)", description: "其他信息如{file.path|n(Path)}或自定义前置字段", - placeholder: "{file.path|n(Path)}", + placeholder: "{file.path|n(路径)}", }, }, quickReference: { @@ -1574,12 +1570,12 @@ export const zh: TranslationTree = { defaultNoteTemplate: { name: "默认笔记模板", description: "从ICS事件创建笔记的模板文件路径", - placeholder: "Templates/Event Template.md", + placeholder: "模板/事件模板.md", }, defaultNoteFolder: { name: "默认笔记文件夹", description: "从ICS事件创建笔记的文件夹", - placeholder: "Calendar/Events", + placeholder: "日历/事件", }, filenameFormat: { name: "ICS笔记文件名格式", @@ -1643,9 +1639,9 @@ export const zh: TranslationTree = { }, placeholders: { calendarName: "日历名称", - url: "ICS/iCal URL", + url: "ICS/iCal 地址", filePath: "本地文件路径(例如,Calendar.ics)", - localFile: "Calendar.ics", + localFile: "日历.ics", }, statusLabels: { enabled: "已启用", @@ -1678,7 +1674,7 @@ export const zh: TranslationTree = { filePath: { name: "导出文件路径", description: "ICS文件保存的路径(相对于库根目录)", - placeholder: "tasknotes-calendar.ics", + placeholder: "tasknotes-日历.ics", }, interval: { name: "更新间隔(5到1440分钟之间)", @@ -1822,7 +1818,7 @@ export const zh: TranslationTree = { noDateToSync: "任务没有可同步的计划日期或截止日期", syncFailed: "同步任务到Google日历失败:{message}", connectionExpired: - "Google Calendar connection expired. Please reconnect in Settings > Integrations.", + "Google 日历连接已过期。请在“设置 > 集成”中重新连接。", syncingTasks: "正在同步{total}个任务到Google日历...", syncComplete: "同步完成:{synced}个已同步,{failed}个失败,{skipped}个跳过", @@ -1857,12 +1853,12 @@ export const zh: TranslationTree = { authToken: { name: "API认证令牌", description: "API认证所需的令牌(留空表示无认证)", - placeholder: "your-secret-token", + placeholder: "你的秘密令牌", }, mcp: { enable: { - name: "Enable MCP Server", - description: "Expose TaskNotes tools via Model Context Protocol at /mcp endpoint. Requires HTTP API to be enabled.", + name: "启用 MCP 服务器", + description: "通过 /mcp 端点使用 Model Context Protocol 暴露 TaskNotes 工具。需要启用 HTTP API。", }, }, endpoints: { @@ -1893,7 +1889,7 @@ export const zh: TranslationTree = { transform: "转换:", }, placeholders: { - url: "Webhook URL", + url: "Webhook 地址", noEventsSelected: "未选择事件", rawPayload: "原始载荷(无转换)", }, @@ -2032,7 +2028,7 @@ export const zh: TranslationTree = { transformSection: "转换配置(可选)", headersSection: "标头配置", url: { - name: "Webhook URL", + name: "Webhook 地址", description: "将发送webhook载荷的端点", placeholder: "https://your-service.com/webhook", }, @@ -2074,10 +2070,10 @@ export const zh: TranslationTree = { }, mdbaseSpec: { header: "mdbase类型定义", - learnMore: "Learn more about mdbase-spec", + learnMore: "了解更多关于 mdbase-spec 的信息", enable: { - name: "Generate mdbase type definitions", - description: "Generate and maintain mdbase type files (mdbase.yaml and _types/task.md) at the vault root as your settings change.", + name: "生成 mdbase 类型定义", + description: "随着设置变化,在库根目录生成并维护 mdbase 类型文件(mdbase.yaml 和 _types/task.md)。", }, }, timeFormats: { @@ -2265,7 +2261,7 @@ export const zh: TranslationTree = { useTemplateDesc: "创建内容时应用模板", templatePathLabel: "模板路径", templatePathDesc: "模板文件的路径", - templatePathPlaceholder: "templates/ics-note-template.md", + templatePathPlaceholder: "模板/ics笔记模板.md", }, unscheduledTasksSelector: { title: "未计划的任务", @@ -3103,7 +3099,7 @@ export const zh: TranslationTree = { scheduledDate: "安排日期", timeEstimate: "时间估计", totalTrackedTime: "总跟踪时间", - checklistProgress: "Checklist Progress", + checklistProgress: "清单进度", recurrence: "重复", completedDate: "完成日期", createdDate: "创建日期", From 3c21143ac9d91a5036c7352df85a7223ef08054a Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 17:12:34 +1000 Subject: [PATCH 24/29] Polish task modal tab navigation setting --- docs/releases/unreleased.md | 5 ++ i18n.manifest.json | 2 - i18n.state.json | 64 -------------------------- src/editor/EmbeddableMarkdownEditor.ts | 10 +++- src/i18n/resources/de.ts | 4 -- src/i18n/resources/en.ts | 4 -- src/i18n/resources/es.ts | 4 -- src/i18n/resources/fr.ts | 4 -- src/i18n/resources/ja.ts | 4 -- src/i18n/resources/ko.ts | 4 -- src/i18n/resources/pt.ts | 4 -- src/i18n/resources/ru.ts | 4 -- src/i18n/resources/zh.ts | 4 -- src/modals/TaskCreationModal.ts | 5 +- src/modals/TaskModal.ts | 39 ++++++++++------ src/modals/taskModalEditorAdapter.ts | 9 ++-- src/settings/defaults.ts | 2 +- src/settings/tabs/featuresTab.ts | 13 ------ src/settings/tabs/modalFieldsTab.ts | 16 +++++-- src/types/settings.ts | 2 +- 20 files changed, 63 insertions(+), 140 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index e35a98636..86c7d77c3 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -24,6 +24,11 @@ Example: --> +## Added + +- (#1777) Added a Modal Fields setting to choose whether Tab/Shift+Tab move focus out of the task details editor or use the markdown editor's indentation behavior. + - Thanks to @P-Sc for the PR. + ## Fixed - (#1765, #1769) Fixed auto-archived tasks leaving stale Google Calendar events when cleanup runs before calendar sync is ready or after the task moves into the archive folder. diff --git a/i18n.manifest.json b/i18n.manifest.json index 672bd06be..3cdcbc94d 100644 --- a/i18n.manifest.json +++ b/i18n.manifest.json @@ -322,8 +322,6 @@ "settings.features.instantConvert.toggle.description": "eed5789a053bf142162f60467c1fa80c1ed7830c", "settings.features.instantConvert.folder.name": "ee5974b000e8f6a84b0ad1dae3a0b0f106b5e7b2", "settings.features.instantConvert.folder.description": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", - "settings.features.switchFocusOnTab.name": "960fb4f9873c1de5b6845c82fb870fbbc6b73b2f", - "settings.features.switchFocusOnTab.description": "76fdb1ff4256e6bdca9bcac2857b5a0ad5c1743c", "settings.features.nlp.header": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "settings.features.nlp.description": "632a9af312f20952951f9f58447b064ffa833a08", "settings.features.nlp.enable.name": "e9b0bfb15d333ee3d894a349ed2a9f9e297be154", diff --git a/i18n.state.json b/i18n.state.json index 70fff83b8..57d8f06f1 100644 --- a/i18n.state.json +++ b/i18n.state.json @@ -1292,14 +1292,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "17486005628baac00ec8c047ea023cb1c42fce50" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "d4898917a4e5398fbb049772a34ed36bfa50ddf1" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "617c809a80a2bf9bd040b9010175474ea3f4a2c8" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "306d0086d726996ee9e2e6f0f9060f7b31dcbbd1" @@ -9570,14 +9562,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "1fb23ba9dabd5414106abea7f0e36b1e845102a5" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "a14785bd58840d00deeda04998e5ff0df6543cac" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "b78dcc6aa72d5dfc8097298b453d3c38725676d3" @@ -17848,14 +17832,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "7b8143d67c72876fb9930fd00b005d09381da471" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "bfcf2376999f1a3e14170c55f02adc8654c7b3d0" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "84a4350357e4181bde8c8e82653da351fee5a445" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "b1e8011b64c3e540c76e7200f83e8d4882c3f2b2" @@ -26126,14 +26102,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "54963b20aa549d3d17d76b9f7bf70f683ae0017a" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "ad1756dc9d71b6ca475c2de9f95e9db424a35532" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "6c2061f8087c23186344b858f78e49805da179c8" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "fc05d42078cafcbbe8df56b1752ab0a541f38dd2" @@ -34404,14 +34372,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "8fc3e9f020fa4481cdf1c7114b8091c2ffd64bdc" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "e64d41a76e45c6935009a3ad15b4351ba89bf83c" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "7578770d53a5253f6693f0738f49883a22e6150e" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "4b046cbf82ae9d360a12fd3c7631834919812a55" @@ -42682,14 +42642,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "74a241bba4f841528a71eee9cdadcf31086c21c2" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "69a2ee93469ee562dd557c698e091e46c1f7ceb2" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "6d4ebba516f2d9cc99ac105a5e20219eab8fad74" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "bcaadfba464e1d20c30c42a9c52e5c711c40366c" @@ -50960,14 +50912,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "79a9fdc5ec3efd53d942c87a77d1024d8bf00326" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "aa7ab02810e0ea2c6ede0462c3983a462cc0769b" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "2cf586d43dd2271e986f53bbf54c18037a38baec" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "2c59d2dd46678464aad80724320867faf0bee86d" @@ -59238,14 +59182,6 @@ "source": "4eb3a6b40716481ba7e89ffebaab20c1e5e89881", "translation": "ec72bd6d2d63718c6c9ccfcf1e88e4ce93402840" }, - "settings.features.switchFocusOnTab.name": { - "source": "9b72eba19c06b7ceb9c15c126a0ffffcf689c8ee", - "translation": "3ff9f0a8532808d51a9398827db789b0b918db05" - }, - "settings.features.switchFocusOnTab.description": { - "source": "a14785bd58840d00deeda04998e5ff0df6543cac", - "translation": "a2843c8634654664d021938f01df7d7a6c6212aa" - }, "settings.features.nlp.header": { "source": "94d1773bc1b2af94b263c23d3b39694a5bab7e88", "translation": "84a852be5572401ef59a98f04c21d40008aac4a4" diff --git a/src/editor/EmbeddableMarkdownEditor.ts b/src/editor/EmbeddableMarkdownEditor.ts index 5629b79e8..d4c4d3b17 100644 --- a/src/editor/EmbeddableMarkdownEditor.ts +++ b/src/editor/EmbeddableMarkdownEditor.ts @@ -103,7 +103,7 @@ export interface MarkdownEditorProps { /** Handler for Escape key */ onEscape?: (editor: EmbeddableMarkdownEditor) => void; /** Handler for Tab key (return false to use default behavior) */ - onTab?: (editor: EmbeddableMarkdownEditor) => boolean; + onTab?: (editor: EmbeddableMarkdownEditor, shift: boolean) => boolean; /** Handler for Ctrl/Cmd+Enter */ onSubmit?: (editor: EmbeddableMarkdownEditor) => void; /** Handler for blur event */ @@ -338,7 +338,13 @@ export class EmbeddableMarkdownEditor extends getEditorBase() { { key: "Tab", run: (cm) => { - return this.options.onTab(this); + return this.options.onTab(this, false); + }, + }, + { + key: "Shift-Tab", + run: (cm) => { + return this.options.onTab(this, true); }, }, ]) diff --git a/src/i18n/resources/de.ts b/src/i18n/resources/de.ts index 64ff07beb..7fe96a641 100644 --- a/src/i18n/resources/de.ts +++ b/src/i18n/resources/de.ts @@ -485,10 +485,6 @@ export const de: TranslationTree = { description: "Ordner, in dem aus Checkboxen konvertierte Aufgaben erstellt werden. Leer lassen, um den Standard-Aufgabenordner zu verwenden. Verwende {{currentNotePath}} für den Ordner der aktuellen Notiz oder {{currentNoteTitle}} für einen Unterordner mit dem Notiztitel.", }, }, - switchFocusOnTab: { - name: "Fokus mit Tabulatortaste wechseln", - description: "Wechsle beim Bearbeiten von Aufgabendetails mit der Tabulatortaste den Eingabefokus", - }, nlp: { header: "Natürliche Sprachverarbeitung", description: "Analysiere Daten, Prioritäten und andere Eigenschaften aus Texteingaben.", diff --git a/src/i18n/resources/en.ts b/src/i18n/resources/en.ts index 534bc8cdf..e31b657b7 100644 --- a/src/i18n/resources/en.ts +++ b/src/i18n/resources/en.ts @@ -486,10 +486,6 @@ export const en: TranslationTree = { "Folder where tasks converted from checkboxes will be created. Leave empty to use the default tasks folder. Use {{currentNotePath}} for the current note's folder, or {{currentNoteTitle}} for a subfolder named after the current note.", }, }, - switchFocusOnTab: { - name: "Switch focus on Tab", - description: "Switch input focus to the next field when presssing the Tab key while editing details of a task", - }, nlp: { header: "Natural Language Processing", description: "Parse dates, priorities, and other properties from text input.", diff --git a/src/i18n/resources/es.ts b/src/i18n/resources/es.ts index 2a04c6010..9752edd04 100644 --- a/src/i18n/resources/es.ts +++ b/src/i18n/resources/es.ts @@ -485,10 +485,6 @@ export const es: TranslationTree = { description: "Carpeta donde se crearán las tareas convertidas desde casillas de verificación. Dejar vacío para usar la carpeta de tareas predeterminada. Usa {{currentNotePath}} para la carpeta de la nota actual, o {{currentNoteTitle}} para una subcarpeta con el título de la nota.", }, }, - switchFocusOnTab: { - name: "Cambia el foco con la tecla Tab", - description: "Al editar los detalles de una tarea, cambia el foco de entrada con la tecla Tab", - }, nlp: { header: "Procesamiento de lenguaje natural", description: "Analiza fechas, prioridades y otras propiedades desde texto de entrada.", diff --git a/src/i18n/resources/fr.ts b/src/i18n/resources/fr.ts index f4efa9f60..5aff89edd 100644 --- a/src/i18n/resources/fr.ts +++ b/src/i18n/resources/fr.ts @@ -485,10 +485,6 @@ export const fr: TranslationTree = { description: "Dossier où les tâches converties depuis les cases à cocher seront créées. Laisser vide pour utiliser le dossier de tâches par défaut. Utilisez {{currentNotePath}} pour le dossier de la note actuelle, ou {{currentNoteTitle}} pour un sous-dossier nommé d'après la note.", }, }, - switchFocusOnTab: { - name: "Déplacer le curseur à l'aide de la touche Tab", - description: "Lors de la modification des détails d'une tâche, utilisez la touche Tab pour déplacer le curseur", - }, nlp: { header: "Traitement du langage naturel", description: "Analyse les dates, priorités et autres propriétés depuis le texte saisi.", diff --git a/src/i18n/resources/ja.ts b/src/i18n/resources/ja.ts index 3cd27f464..5aeb6722f 100644 --- a/src/i18n/resources/ja.ts +++ b/src/i18n/resources/ja.ts @@ -485,10 +485,6 @@ export const ja: TranslationTree = { description: "チェックボックスから変換されたタスクが作成されるフォルダー。空白のままにするとデフォルトのタスクフォルダーが使用されます。{{currentNotePath}}で現在のノートのフォルダー、{{currentNoteTitle}}でノートのタイトルを持つサブフォルダーを指定できます。", }, }, - switchFocusOnTab: { - name: "Tabキーでフォーカスを移動します", - description: "タスクの詳細を編集する際は、Tabキーで入力フォーカスを移動してください", - }, nlp: { header: "自然言語処理", description: "テキスト入力から日付、優先度、その他のプロパティを解析します。", diff --git a/src/i18n/resources/ko.ts b/src/i18n/resources/ko.ts index f4fd86a4a..68d50fbd1 100644 --- a/src/i18n/resources/ko.ts +++ b/src/i18n/resources/ko.ts @@ -481,10 +481,6 @@ export const ko: TranslationTree = { description: "체크박스에서 변환된 작업이 생성될 폴더. 기본 작업 폴더를 사용하려면 비워두세요. {{currentNotePath}}는 현재 노트 폴더, {{currentNoteTitle}}은 현재 노트 이름의 하위 폴더입니다.", }, }, - switchFocusOnTab: { - name: "탭 키를 사용하여 포커스를 이동합니다", - description: "작업 세부 정보를 편집할 때 탭 키를 사용하여 입력 포커스를 이동합니다", - }, nlp: { header: "자연어 처리", description: "텍스트 입력에서 날짜, 우선순위 및 기타 속성을 파싱합니다.", diff --git a/src/i18n/resources/pt.ts b/src/i18n/resources/pt.ts index 514d6bb7b..bfefc6676 100644 --- a/src/i18n/resources/pt.ts +++ b/src/i18n/resources/pt.ts @@ -485,10 +485,6 @@ export const pt: TranslationTree = { description: "Pasta onde tarefas convertidas de caixas de seleção serão criadas. Deixe vazio para usar a pasta de tarefas padrão. Use {{currentNotePath}} para a pasta da nota atual, ou {{currentNoteTitle}} para uma subpasta com o título da nota." } }, - switchFocusOnTab: { - name: "Alterar o foco com a tecla Tab", - description: "Ao editar os detalhes de uma tarefa, utilize a tecla Tab para alterar o foco de entrada", - }, nlp: { header: "Processamento de Linguagem Natural", description: "Analisa datas, prioridades e outras propriedades do texto inserido.", diff --git a/src/i18n/resources/ru.ts b/src/i18n/resources/ru.ts index fe16e9bf4..d14995dca 100644 --- a/src/i18n/resources/ru.ts +++ b/src/i18n/resources/ru.ts @@ -485,10 +485,6 @@ export const ru: TranslationTree = { description: "Папка, в которой будут создаваться задачи, преобразованные из флажков. Оставьте пустым для использования папки задач по умолчанию. Используйте {{currentNotePath}} для папки текущей заметки или {{currentNoteTitle}} для подпапки с названием заметки.", }, }, - switchFocusOnTab: { - name: "Перемещайте фокус с помощью клавиши Tab", - description: "При редактировании сведений о задании перемещайте фокус ввода с помощью клавиши Tab", - }, nlp: { header: "Обработка естественного языка", description: "Анализ дат, приоритетов и других свойств из текстового ввода.", diff --git a/src/i18n/resources/zh.ts b/src/i18n/resources/zh.ts index 1959e319a..38ecb86e3 100644 --- a/src/i18n/resources/zh.ts +++ b/src/i18n/resources/zh.ts @@ -485,10 +485,6 @@ export const zh: TranslationTree = { description: "从复选框转换的任务将在其中创建的文件夹。留空则使用默认任务文件夹。使用{{currentNotePath}}表示当前笔记的文件夹,或使用{{currentNoteTitle}}表示以笔记标题命名的子文件夹。", }, }, - switchFocusOnTab: { - name: "使用 Tab 键切换焦点", - description: "在编辑任务详情时,使用 Tab 键切换输入焦点", - }, nlp: { header: "自然语言处理", description: "从文本输入解析日期、优先级和其他属性。", diff --git a/src/modals/TaskCreationModal.ts b/src/modals/TaskCreationModal.ts index e8c9debe6..4a0748037 100644 --- a/src/modals/TaskCreationModal.ts +++ b/src/modals/TaskCreationModal.ts @@ -748,7 +748,10 @@ export class TaskCreationModal extends TaskModal { // Vim mode will handle its own ESC to exit insert mode this.close(); }, - onTab: () => { + onTab: (shift) => { + if (shift) { + return false; + } // Tab - jump to title input (expand form if needed) if (!this.isExpanded) { this.expandModal(); diff --git a/src/modals/TaskModal.ts b/src/modals/TaskModal.ts index 2b15fb2f2..356502b32 100644 --- a/src/modals/TaskModal.ts +++ b/src/modals/TaskModal.ts @@ -789,10 +789,6 @@ export abstract class TaskModal extends Modal { // Create container for the markdown editor const detailsEditorContainer = rightColumn.createDiv("details-markdown-editor"); - const onTabCallback = this.plugin.settings.switchFocusOnTab - ? () => {this.focusNextField(); return true;} // jump to next input field & prevent default tab behavior - : () => false; // default behavior - // Create embeddable markdown editor for details using shared method this.detailsMarkdownEditor = createTaskModalMarkdownEditor(this.app, detailsEditorContainer, { value: this.details, @@ -809,7 +805,13 @@ export abstract class TaskModal extends Modal { // ESC - close the modal this.close(); }, - onTab: onTabCallback, + onTab: (shift) => { + if (!this.plugin.settings.taskModalTabMovesFocus) { + return false; + } + + return shift ? this.focusPreviousField() : this.focusNextField(); + }, }); } @@ -1981,17 +1983,28 @@ export abstract class TaskModal extends Modal { return this.title.trim().length > 0; } - protected focusNextField(): void { + protected focusNextField(): boolean { // Try to focus the contexts input as the next field after details + const nextField = this.contextsInput || this.tagsInput || this.timeEstimateInput; + if (!nextField) { + return false; + } + setTimeout(() => { - if (this.contextsInput) { - this.contextsInput.focus(); - } else if (this.tagsInput) { - this.tagsInput.focus(); - } else if (this.timeEstimateInput) { - this.timeEstimateInput.focus(); - } + nextField.focus(); + }, 50); + return true; + } + + protected focusPreviousField(): boolean { + if (!this.titleInput) { + return false; + } + + setTimeout(() => { + this.titleInput?.focus(); }, 50); + return true; } onClose(): void { diff --git a/src/modals/taskModalEditorAdapter.ts b/src/modals/taskModalEditorAdapter.ts index 4dbaa9069..26dcfc46f 100644 --- a/src/modals/taskModalEditorAdapter.ts +++ b/src/modals/taskModalEditorAdapter.ts @@ -8,7 +8,7 @@ export interface TaskModalEditorOptions { onChange: (value: string) => void; onSubmit: () => void; onEscape: () => void; - onTab: () => boolean; + onTab: (shift: boolean) => boolean; extensions?: any[]; } @@ -18,7 +18,10 @@ export function createTaskModalMarkdownEditor( options: TaskModalEditorOptions ): EmbeddableMarkdownEditor | null { try { - return new EmbeddableMarkdownEditor(app, container, options); + return new EmbeddableMarkdownEditor(app, container, { + ...options, + onTab: (_editor, shift) => options.onTab(shift), + }); } catch (error) { console.error("Failed to create markdown editor:", error); @@ -38,7 +41,7 @@ export function createTaskModalMarkdownEditor( e.preventDefault(); options.onEscape(); } else if (e.key === "Tab") { - const shouldPreventDefault = options.onTab(); + const shouldPreventDefault = options.onTab(e.shiftKey); if (shouldPreventDefault) { e.preventDefault(); } diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 9ea628f0c..6e11b929c 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -295,7 +295,7 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = { disableOverlayOnAlias: false, enableInstantTaskConvert: true, useDefaultsOnInstantConvert: true, - switchFocusOnTab: true, + taskModalTabMovesFocus: true, enableNaturalLanguageInput: true, nlpDefaultToScheduled: true, nlpLanguage: "en", // Default to English diff --git a/src/settings/tabs/featuresTab.ts b/src/settings/tabs/featuresTab.ts index d691f4e5b..e79c2cea8 100644 --- a/src/settings/tabs/featuresTab.ts +++ b/src/settings/tabs/featuresTab.ts @@ -108,19 +108,6 @@ export function renderFeaturesTab( }, }) ); - - group.addSetting((setting) => - configureToggleSetting(setting, { - name: translate("settings.features.switchFocusOnTab.name"), - desc: translate("settings.features.switchFocusOnTab.description"), - getValue: () => plugin.settings.switchFocusOnTab, - setValue: async (value: boolean) => { - plugin.settings.switchFocusOnTab = value; - save(); - renderFeaturesTab(container, plugin, save); - }, - }) - ); } ); diff --git a/src/settings/tabs/modalFieldsTab.ts b/src/settings/tabs/modalFieldsTab.ts index c91530e82..d0efae0d8 100644 --- a/src/settings/tabs/modalFieldsTab.ts +++ b/src/settings/tabs/modalFieldsTab.ts @@ -1,6 +1,5 @@ import { Notice } from "obsidian"; import TaskNotesPlugin from "../../main"; -import { TranslationKey } from "../../i18n"; import { createSettingGroup, configureToggleSetting } from "../components/settingHelpers"; import { createFieldManager, addFieldManagerStyles } from "../components/FieldManagerComponent"; import { initializeFieldConfig } from "../../utils/fieldConfigDefaults"; @@ -17,9 +16,6 @@ export function renderModalFieldsTab( ): void { container.empty(); - const translate = (key: TranslationKey, params?: Record) => - plugin.i18n.translate(key, params); - // Add styles for field manager addFieldManagerStyles(); @@ -53,6 +49,18 @@ export function renderModalFieldsTab( }) ); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: "Tab moves focus in details editor", + desc: "When enabled, Tab moves from the details editor to the next modal field and Shift+Tab moves to the previous field. When disabled, Tab and Shift+Tab use the markdown editor's indentation behavior.", + getValue: () => plugin.settings.taskModalTabMovesFocus, + setValue: (value) => { + plugin.settings.taskModalTabMovesFocus = value; + save(); + }, + }) + ); + // Sync button group.addSetting((setting) => { setting diff --git a/src/types/settings.ts b/src/types/settings.ts index 3bc5da44e..5471de19b 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -127,7 +127,7 @@ export interface TaskNotesSettings { disableOverlayOnAlias: boolean; enableInstantTaskConvert: boolean; useDefaultsOnInstantConvert: boolean; - switchFocusOnTab: boolean; + taskModalTabMovesFocus: boolean; enableNaturalLanguageInput: boolean; nlpDefaultToScheduled: boolean; nlpLanguage: string; // Language code for natural language processing (e.g., 'en', 'es', 'fr') From c69a89a5f7576db83fa0e88c1ac5ad2768a22503 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 17:15:18 +1000 Subject: [PATCH 25/29] Fix Bases task card property labels --- docs/releases/unreleased.md | 2 + src/bases/helpers.ts | 48 +++++++++++++++---- src/ui/taskCardHelpers.ts | 30 +++++++++++- ...-1633-task-card-i18n-field-mapping.test.ts | 42 ++++++++++++++++ 4 files changed, 113 insertions(+), 9 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index fb5eb84d9..e76d33d7d 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -26,6 +26,8 @@ Example: ## Fixed +- (#1633) Fixed Bases task cards using fallback labels instead of Bases display names for mapped task fields and file properties. + - Thanks to @Sarryaz for reporting the Bases task card i18n and field-mapping issue. - (#884) Fixed untranslated strings and English placeholder examples across non-English interface translations. - Thanks to @berzernberg for reporting Russian translation gaps. - (#1765, #1769) Fixed auto-archived tasks leaving stale Google Calendar events when cleanup runs before calendar sync is ready or after the task moves into the archive folder. diff --git a/src/bases/helpers.ts b/src/bases/helpers.ts index cdbea22de..78a2ae030 100644 --- a/src/bases/helpers.ts +++ b/src/bases/helpers.ts @@ -6,6 +6,7 @@ import { calculateTotalTimeSpent } from "../utils/helpers"; import { format } from "date-fns"; import { convertInternalToUserProperties } from "../utils/propertyMapping"; import { DEFAULT_INTERNAL_VISIBLE_PROPERTIES } from "../settings/defaults"; +import type { TaskCardOptions } from "../ui/TaskCard"; export interface BasesDataItem { key?: string; @@ -237,6 +238,27 @@ interface BasesSelectedProperty { visible: boolean; } +function buildTaskCardPropertyLabels( + basesVisibleProperties: BasesSelectedProperty[], + plugin: TaskNotesPlugin +): NonNullable { + const labels: NonNullable = {}; + + for (const property of basesVisibleProperties) { + const displayName = property.displayName?.trim(); + if (!displayName) { + continue; + } + + const taskCardPropertyId = mapBasesPropertyToTaskCardProperty(property.id, plugin); + if (taskCardPropertyId) { + labels[taskCardPropertyId] = displayName; + } + } + + return labels; +} + export function getBasesVisibleProperties(basesContainer: any): BasesSelectedProperty[] { try { const controller = (basesContainer?.controller ?? basesContainer) as any; @@ -327,7 +349,8 @@ export async function renderTaskNotesInBasesView( plugin: TaskNotesPlugin, basesContainer?: any, taskElementsMap?: Map, - precomputedVisibleProperties?: string[] + precomputedVisibleProperties?: string[], + precomputedCardOptions?: Partial ): Promise { console.log("[TaskNotes][Bases] renderTaskNotesInBasesView ENTRY - tasks:", taskNotes.length, "basesContainer:", !!basesContainer, "precomputed props:", precomputedVisibleProperties?.length); const { createTaskCard } = await import("../ui/TaskCard"); @@ -341,7 +364,7 @@ export async function renderTaskNotesInBasesView( // Get visible properties from Bases let visibleProperties: string[] | undefined = precomputedVisibleProperties; - let cardOptions = {}; + let cardOptions: Partial = precomputedCardOptions || {}; // Only extract properties if not precomputed if (!visibleProperties && basesContainer) { @@ -350,6 +373,11 @@ export async function renderTaskNotesInBasesView( console.log("[TaskNotes][Bases] getBasesVisibleProperties returned:", basesVisibleProperties.length, "properties"); if (basesVisibleProperties.length > 0) { + cardOptions = { + ...cardOptions, + propertyLabels: buildTaskCardPropertyLabels(basesVisibleProperties, plugin), + }; + // Extract just the property IDs for TaskCard visibleProperties = basesVisibleProperties.map((p) => p.id); console.log("[TaskNotes][Bases] Raw property IDs from Bases:", visibleProperties); @@ -444,8 +472,16 @@ export async function renderGroupedTasksInBasesView( // Get visible properties from Bases FIRST (needed for both grouped and ungrouped rendering) const basesVisibleProperties = getBasesVisibleProperties(viewContext); let visibleProperties: string[] | undefined; + let cardOptions: Partial = { + targetDate: new Date(), + }; if (basesVisibleProperties.length > 0) { + cardOptions = { + ...cardOptions, + propertyLabels: buildTaskCardPropertyLabels(basesVisibleProperties, plugin), + }; + visibleProperties = basesVisibleProperties.map((p) => p.id); console.log("[TaskNotes][Bases][Grouped] Raw property IDs from Bases:", visibleProperties); @@ -494,7 +530,7 @@ export async function renderGroupedTasksInBasesView( const groupedData = viewContext?.data?.groupedData; if (!Array.isArray(groupedData) || groupedData.length === 0) { // No groups, fall back to flat rendering (pass precomputed properties) - await renderTaskNotesInBasesView(container, taskNotes, plugin, viewContext, taskElementsMap, visibleProperties); + await renderTaskNotesInBasesView(container, taskNotes, plugin, viewContext, taskElementsMap, visibleProperties, cardOptions); return; } @@ -506,7 +542,7 @@ export async function renderGroupedTasksInBasesView( // If the key is null, undefined, empty string, or "Unknown", treat as ungrouped if (groupKey === null || groupKey === undefined || groupKey === "" || groupKeyStr === "null" || groupKeyStr === "undefined" || groupKeyStr === "Unknown") { // Render as flat list without group headers (pass precomputed properties) - await renderTaskNotesInBasesView(container, taskNotes, plugin, viewContext, taskElementsMap, visibleProperties); + await renderTaskNotesInBasesView(container, taskNotes, plugin, viewContext, taskElementsMap, visibleProperties, cardOptions); return; } } @@ -519,10 +555,6 @@ export async function renderGroupedTasksInBasesView( listWrapper.className = "tn-bases-tasknotes-list"; container.appendChild(listWrapper); - const cardOptions = { - targetDate: new Date(), - }; - // Create a map from file path to TaskInfo for quick lookup const tasksByPath = new Map(); taskNotes.forEach((task) => { diff --git a/src/ui/taskCardHelpers.ts b/src/ui/taskCardHelpers.ts index 38afebc1d..e7fd45caf 100644 --- a/src/ui/taskCardHelpers.ts +++ b/src/ui/taskCardHelpers.ts @@ -41,6 +41,12 @@ export function getTaskCardPropertyLabel( plugin: TaskNotesPlugin, propertyLabels?: Record ): string { + const directOverride = propertyLabels?.[propertyId]; + if (directOverride && directOverride.trim() !== "") { + return directOverride; + } + + const mappedOverride = getMappedPropertyLabel(propertyId, plugin, propertyLabels); const fallbackLabels: Record = { due: tTaskCard(plugin, "labels.due"), scheduled: tTaskCard(plugin, "labels.scheduled"), @@ -54,11 +60,33 @@ export function getTaskCardPropertyLabel( return resolveTaskCardPropertyLabel( propertyId, - { propertyLabels }, + { propertyLabels: mappedOverride ? { [propertyId]: mappedOverride } : propertyLabels }, fallbackLabels[propertyId] ); } +function getMappedPropertyLabel( + propertyId: string, + plugin: TaskNotesPlugin, + propertyLabels?: Record +): string | undefined { + if (!propertyLabels) { + return undefined; + } + + for (const [candidatePropertyId, label] of Object.entries(propertyLabels)) { + if (candidatePropertyId === propertyId || label.trim() === "") { + continue; + } + + if (plugin.fieldMapper?.lookupMappingKey?.(candidatePropertyId) === propertyId) { + return label; + } + } + + return undefined; +} + export function getRecurrenceTooltip( plugin: TaskNotesPlugin, recurrence: string, diff --git a/tests/unit/issues/issue-1633-task-card-i18n-field-mapping.test.ts b/tests/unit/issues/issue-1633-task-card-i18n-field-mapping.test.ts index 3de893bb0..f97c33ebd 100644 --- a/tests/unit/issues/issue-1633-task-card-i18n-field-mapping.test.ts +++ b/tests/unit/issues/issue-1633-task-card-i18n-field-mapping.test.ts @@ -14,12 +14,37 @@ import { describe, expect, it } from "@jest/globals"; import * as fs from "fs"; import * as path from "path"; +import { getTaskCardPropertyLabel } from "../../../src/ui/taskCardHelpers"; import { resolveTaskCardPropertyLabel } from "../../../src/ui/taskCardPresentation"; function readRepoFile(relativePath: string): string { return fs.readFileSync(path.resolve(__dirname, "../../../", relativePath), "utf8"); } +function createLabelPlugin(mapping: Record): any { + return { + fieldMapper: { + lookupMappingKey: (propertyId: string) => mapping[propertyId] ?? null, + }, + i18n: { + translate: (key: string) => { + const translations: Record = { + "ui.taskCard.labels.due": "Due fallback", + "ui.taskCard.labels.scheduled": "Scheduled fallback", + "ui.taskCard.labels.recurrence": "Recurring fallback", + "ui.taskCard.labels.completed": "Completed fallback", + "ui.taskCard.labels.created": "Created fallback", + "ui.taskCard.labels.modified": "Modified fallback", + "ui.taskCard.labels.blocked": "Blocked fallback", + "ui.taskCard.labels.blocking": "Blocking fallback", + }; + + return translations[key] ?? key; + }, + }, + }; +} + describe("Issue #1633: TaskCard label localization + property display names", () => { it("uses presentation helpers and translation-backed labels instead of hardcoded strings", () => { const taskCardSource = readRepoFile("src/ui/TaskCard.ts"); @@ -40,4 +65,21 @@ describe("Issue #1633: TaskCard label localization + property display names", () resolveTaskCardPropertyLabel("file.tags", { propertyLabels: { "file.tags": "Tags" } }) ).toBe("Tags"); }); + + it("uses Bases display names for user-mapped TaskCard renderer labels", () => { + const plugin = createLabelPlugin({ + faellig: "due", + geplant: "scheduled", + wiederholung: "recurrence", + }); + const propertyLabels = { + faellig: "Fällig", + geplant: "Geplant", + wiederholung: "Wiederholung", + }; + + expect(getTaskCardPropertyLabel("due", plugin, propertyLabels)).toBe("Fällig"); + expect(getTaskCardPropertyLabel("scheduled", plugin, propertyLabels)).toBe("Geplant"); + expect(getTaskCardPropertyLabel("recurrence", plugin, propertyLabels)).toBe("Wiederholung"); + }); }); From d24300b41a60a5c78e93bc7af65053df1d4fb308 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 26 Apr 2026 17:20:07 +1000 Subject: [PATCH 26/29] Update docs builder theme --- .gitignore | 15 ++- docs-builder/src/styles/main.css | 209 +++++++++++++++++++++++-------- docs-builder/src/template.html | 16 ++- 3 files changed, 180 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 8ba6b6b32..17826afce 100644 --- a/.gitignore +++ b/.gitignore @@ -82,9 +82,12 @@ tasknotes-e2e-vault/.obsidian/workspace.json .obsidian-config-e2e # Generated calendar files tasknotes-e2e-vault/*.ics -/.ops -AGENTS.md -/docs-builder/dist -.ops/ -.serena/ -/.codex +/.ops +AGENTS.md +/docs-builder/dist +.ops/ +.serena/ +/.codex + +# Hyperframes release-announcement videos (assets + renders) +/release-videos/ diff --git a/docs-builder/src/styles/main.css b/docs-builder/src/styles/main.css index 0de66afa7..1ab64e4a7 100644 --- a/docs-builder/src/styles/main.css +++ b/docs-builder/src/styles/main.css @@ -1,28 +1,33 @@ /* ============================================================ TaskNotes Documentation - Aesthetic: Scholarly Reference / Technical Manual + Aesthetic: friendly chrome around scholarly content. ────────────────────────────────────────────────────────── - Display — Cormorant Garamond (refined optical serif) - Body — IBM Plex Serif (technical warmth) - UI / Nav — IBM Plex Mono (the weird part: the sidebar - reads like a file listing - while content reads like - a typeset book) + Brand — Bungee (logotype + h1 — chunky display) + Friendly — Comic Neue (h2, occasional CTA — the warmth) + Body — IBM Plex Serif (long-form reading) + UI / Nav — IBM Plex Mono (sidebar reads like a directory) ────────────────────────────────────────────────────────── - One accent colour: amber. Used sparingly. + Accent: pink (with a cyan secondary used sparingly). + The TaskNotes icon stands in for typographic ornaments. ============================================================ */ -@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=IBM+Plex+Mono:ital,wght@0,400;0,500;1,400&family=IBM+Plex+Serif:ital,wght@0,300;0,400;1,300;1,400&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Bungee&family=Comic+Neue:wght@400;700&family=IBM+Plex+Mono:ital,wght@0,400;0,500;1,400&family=IBM+Plex+Serif:ital,wght@0,300;0,400;0,500;1,300;1,400&display=swap'); /* ============================================================ Tokens ============================================================ */ :root { - --font-display : 'Cormorant Garamond', Georgia, serif; --font-body : 'IBM Plex Serif', Georgia, serif; --font-mono : 'IBM Plex Mono', 'Courier New', monospace; + --font-marquee : 'Bungee', 'Comic Neue', 'Comic Sans MS', cursive; + --font-friendly: 'Comic Neue', 'Comic Sans MS', cursive; + + /* TaskNotes icon as a CSS mask — for tiny inline glyphs. + The full logo's negative-space cuts muddy at <14px, so this is + the silhouette path only. Color via background-color. */ + --tn-mask: url('data:image/svg+xml;utf8,'); /* Light */ --bg : #f9f7f4; @@ -34,9 +39,14 @@ --text-faint : #a09890; --border : #e0dbd4; --border-strong : #c8c2ba; - --amber : #b76b1c; - --amber-bg : rgba(183, 107, 28, 0.08); - --amber-border : rgba(183, 107, 28, 0.25); + /* The accent — kept under the legacy "--amber" name so all existing + selectors pick up the new value without churn. The hue is now pink, + paired with a cyan secondary used sparingly (sidebar accent stripe). */ + --amber : #d92a85; + --amber-bg : rgba(217, 42, 133, 0.08); + --amber-border : rgba(217, 42, 133, 0.25); + --cyan : #0a9bb8; + --ink : #1a1714; --sidebar-w : 256px; --content-max: 680px; @@ -53,9 +63,11 @@ --text-faint : #564e48; --border : #2d2925; --border-strong : #403b36; - --amber : #d4832a; - --amber-bg : rgba(212, 131, 42, 0.08); - --amber-border : rgba(212, 131, 42, 0.25); + --amber : #ff5fa3; + --amber-bg : rgba(255, 95, 163, 0.08); + --amber-border : rgba(255, 95, 163, 0.28); + --cyan : #2db4cf; + --ink : #e6e2da; } /* ============================================================ @@ -157,6 +169,58 @@ body { min-width: 0; display: flex; flex-direction: column; + position: relative; + overflow: hidden; +} + +/* Drifting TaskNotes-icon ornaments — quiet ambient personality. + Two pseudo-element floaters; lower opacity than the mockup so they + don't compete with long-form reading. */ +.stage::before, +.stage::after { + content: ""; + position: absolute; + pointer-events: none; + background: var(--amber); + -webkit-mask: var(--tn-mask) center / contain no-repeat; + mask: var(--tn-mask) center / contain no-repeat; + z-index: 0; +} + +.stage::before { + top: 100px; + right: 60px; + width: 56px; + height: 56px; + opacity: 0.18; + animation: tn-drift-a 22s ease-in-out infinite; +} + +.stage::after { + top: 540px; + right: 110px; + width: 36px; + height: 36px; + opacity: 0.12; + background: var(--cyan); + animation: tn-drift-b 28s ease-in-out infinite; +} + +@keyframes tn-drift-a { + 0%, 100% { transform: translate(0, 0) rotate(-8deg); } + 50% { transform: translate(-14px, 18px) rotate(10deg); } +} +@keyframes tn-drift-b { + 0%, 100% { transform: translate(0, 0) rotate(6deg); } + 50% { transform: translate(18px, -12px) rotate(-10deg); } +} + +@media (prefers-reduced-motion: reduce) { + .stage::before, + .stage::after, + .logotype__mark { + animation: none; + } } /* Content row: prose + TOC */ @@ -165,6 +229,8 @@ body { flex: 1; gap: 0; justify-content: center; + position: relative; + z-index: 1; } /* ============================================================ @@ -173,18 +239,41 @@ body { .logotype { display: flex; - flex-direction: column; - gap: 0.1em; + align-items: center; + gap: 0.7rem; text-decoration: none; color: var(--text); } +.logotype__mark { + width: 32px; + height: 32px; + flex-shrink: 0; + color: var(--amber); + /* gentle bob — present, not loud */ + animation: tn-bob 5s ease-in-out infinite; + transform-origin: center; +} + +@keyframes tn-bob { + 0%, 100% { transform: rotate(-4deg); } + 50% { transform: rotate(4deg); } +} + +.logotype__text { + display: flex; + flex-direction: column; + gap: 0.1em; + min-width: 0; +} + .logotype__name { - font-family: var(--font-display); - font-size: 1.3rem; - font-weight: 500; - letter-spacing: -0.01em; + font-family: var(--font-marquee); + font-size: 1.05rem; + font-weight: 400; + letter-spacing: 0.005em; line-height: 1; + color: var(--text); } .logotype__sub { @@ -229,6 +318,18 @@ body { font-weight: 500; } +.nav-link.is-active::after { + content: ""; + display: inline-block; + width: 10px; + height: 10px; + margin-left: 0.45em; + vertical-align: -1px; + background: var(--amber); + -webkit-mask: var(--tn-mask) center / contain no-repeat; + mask: var(--tn-mask) center / contain no-repeat; +} + /* Section groups */ .nav-section { margin-top: 1.5rem; @@ -356,12 +457,12 @@ body { } .topbar__title { - font-family: var(--font-display); - font-size: 1.1rem; - font-weight: 500; + font-family: var(--font-marquee); + font-size: 1rem; + font-weight: 400; color: var(--text); text-decoration: none; - letter-spacing: -0.01em; + letter-spacing: 0.005em; } .menu-btn { @@ -386,21 +487,22 @@ body { } .page-title { - font-family: var(--font-display); + font-family: var(--font-marquee); font-weight: 400; - font-size: 2.8rem; - letter-spacing: -0.02em; - line-height: 1.1; - margin-bottom: 2.5rem; + font-size: 2.4rem; + letter-spacing: 0.005em; + line-height: 1; + margin-bottom: 2.25rem; color: var(--text); + text-shadow: 3px 3px 0 var(--amber); } /* Headings */ .prose h2 { - font-family: var(--font-display); - font-weight: 400; - font-size: 1.9rem; - letter-spacing: -0.02em; + font-family: var(--font-friendly); + font-weight: 700; + font-size: 1.55rem; + letter-spacing: 0; line-height: 1.2; margin-top: 3.5rem; margin-bottom: 1rem; @@ -408,24 +510,27 @@ body { position: relative; } -/* The ornament: a small amber marker before each h2. - Reads like a typographic chapter marker. */ +/* The ornament: a small TaskNotes-icon glyph before each h2, + sitting on the same baseline as the heading text. */ .prose h2::before { - content: '—'; - display: block; - font-family: var(--font-mono); - font-size: 0.7rem; - color: var(--amber); - letter-spacing: 0.2em; - margin-bottom: 0.5rem; - opacity: 0.8; + content: ""; + display: inline-block; + width: 0.85em; + height: 0.85em; + margin-right: 0.5em; + vertical-align: -0.05em; + background: var(--amber); + -webkit-mask: var(--tn-mask) center / contain no-repeat; + mask: var(--tn-mask) center / contain no-repeat; + opacity: 0.9; } .prose h3 { - font-family: var(--font-display); - font-weight: 400; - font-size: 1.3rem; - letter-spacing: -0.01em; + font-family: var(--font-body); + font-weight: 500; + font-style: italic; + font-size: 1.2rem; + letter-spacing: 0; line-height: 1.3; margin-top: 2.5rem; margin-bottom: 0.75rem; @@ -731,9 +836,9 @@ body { } .card__title { - font-family: var(--font-display); - font-size: 1.05rem; - font-weight: 500; + font-family: var(--font-friendly); + font-size: 1.02rem; + font-weight: 700; color: var(--text); line-height: 1.2; } diff --git a/docs-builder/src/template.html b/docs-builder/src/template.html index 7283ed970..392b9173f 100644 --- a/docs-builder/src/template.html +++ b/docs-builder/src/template.html @@ -21,8 +21,20 @@