Skip to content

feat(calendar): natural language time parsing in event title - #8505

Open
miaulalala wants to merge 1 commit into
mainfrom
feat/natural-language-time-parsing
Open

feat(calendar): natural language time parsing in event title#8505
miaulalala wants to merge 1 commit into
mainfrom
feat/natural-language-time-parsing

Conversation

@miaulalala

@miaulalala miaulalala commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds natural language time/date parsing to the event title field in both the popover (EditSimple) and full (EditFull) editors. When the user types a recognised time or date expression, a dismissible suggestion banner appears below the title input.

  • Apply (button or Enter key): sets the event time/date and strips the matched text from the title
  • Dismiss (button or Escape key): hides the banner without making changes

Supported patterns:

  • Time only: 2pm, 14:00, 9am
  • Time range: 10:00 - 14:00, 10am to 2pm
  • Date (→ all-day): July 23, 23.07.26, 07/23/2026
  • Date range (→ all-day): June 1 to June 3, June 1-3
  • Date + time: July 23 4pm, 14.08.2026 19:00
  • All day: all day, all-day
  • Relative: tomorrow, next Monday, this Friday, in 3 days, next week
  • Weekday + time: next Friday 14:00

Multi-language support via chrono-node locale routing based on getLanguage(): de, fr, es, it, nl, pt, ru, uk, sv, fi, ja, vi, zh-hans, zh-hant, with English fallback for unsupported locales.

Implementation:

  • src/services/naturalLanguageTimeParserService.js — parsing logic using chrono-node with a custom European DD.MM.YY parser prepended for priority
  • src/components/Editor/Properties/TimeSuggestion.vue — suggestion banner component
  • src/mixins/TimeSuggestionMixin.js — shared mixin consumed by both editor views
  • 94 unit tests covering all pattern types, stripping positions, locale handling, and relative expressions

Screenshot

image

AI disclosure

This PR was developed with assistance from Claude Code (claude-sonnet-4-6).

Test plan

  • Open a new event in the popover editor, type 2pm in the title → suggestion banner appears showing "Set time: 14:00"
  • Press Enter → time is set, "2pm" is removed from the title
  • Type all day → banner shows "Mark as all-day event", apply toggles all-day
  • Type June 1 to June 3 → banner shows date range, apply sets both dates as all-day
  • Type tomorrow 3pm → banner shows datetime, apply sets date and time
  • Type next Monday → banner shows date, apply sets all-day on that Monday
  • Press Escape → banner dismissed without changes
  • Repeat key flows in the full editor (EditFull)
  • With Nextcloud set to German, type 15. Juli → suggestion appears
  • Run unit tests: npm run test:unit

@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/services/naturalLanguageTimeParserService.js 96.15% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Parses times and dates typed into the event title, e.g. 'lunch tomorrow at
1pm', and offers them as a dismissible suggestion that fills the start and end
fields. Uses chrono-node, with a European day-first date parser prepended so
'01/02' reads as 1 February.

Adds naturalLanguageTimeParserService, a TimeSuggestion component and a
TimeSuggestionMixin, wired into both the full and simple editors, plus 94 unit
tests.

AI-Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Anna Larch <anna@nextcloud.com>
@miaulalala
miaulalala force-pushed the feat/natural-language-time-parsing branch from a63d384 to 8102b13 Compare September 1, 2026 18:20
@hamza221
hamza221 self-requested a review September 2, 2026 10:22

<template>
<div class="time-suggestion" role="status" aria-live="polite">
<ClockOutlineIcon :size="16" class="time-suggestion__icon" decorative />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<ClockOutlineIcon :size="16" class="time-suggestion__icon" decorative />
<ClockOutlineIcon :size="20" class="time-suggestion__icon" decorative />

We try to keep them 20 when possible

Comment on lines +38 to +53
const chronoInstances = {
en: buildInstance(chrono.en),
de: buildInstance(chrono.de),
fr: buildInstance(chrono.fr),
es: buildInstance(chrono.es),
it: buildInstance(chrono.it),
nl: buildInstance(chrono.nl),
pt: buildInstance(chrono.pt),
ru: buildInstance(chrono.ru),
uk: buildInstance(chrono.uk),
sv: buildInstance(chrono.sv),
fi: buildInstance(chrono.fi),
ja: buildInstance(chrono.ja),
vi: buildInstance(chrono.vi),
'zh-hans': buildInstance(chrono.zh.hans),
'zh-hant': buildInstance(chrono.zh.hant),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we only support these languages here? Not a critique, just trying to understand

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - the library has a limited set of languages, so that's why they're listed instead of being a generalised feature

@GVodyanov

Copy link
Copy Markdown
Contributor

I'm noting that the suggestion is given to me in locale, which I guess makes sense, but the way we handle it isn't very unified

image

Because I am typing in English, suggestion in Italian, then the actual month in the picker is set in English

If I try to type in Italian, then nothing gets suggested

image

@miaulalala

Copy link
Copy Markdown
Contributor Author

I'm noting that the suggestion is given to me in locale, which I guess makes sense, but the way we handle it isn't very unified
image

Because I am typing in English, suggestion in Italian, then the actual month in the picker is set in English

If I try to type in Italian, then nothing gets suggested
image

ah yes, that's a strange way to handle it. I'll work on it. Thanks for your review!

Comment on lines +138 to +154
if (end && end.isCertain('hour')) {
// Time range: "10:00 – 14:00", "10am to 2pm"
const sh = start.get('hour')
const sm = start.get('minute') ?? 0
const eh = end.get('hour')
const em = end.get('minute') ?? 0
return {
type: 'time-range',
startHour: sh,
startMinute: sm,
endHour: eh,
endMinute: em,
displayText: `${formatTimeParts(sh, sm)} – ${formatTimeParts(eh, em)}`,
matchedText: result.text,
matchedIndex: result.index,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When time range is detected, date is dropped

No time range time range
Image Image

@@ -0,0 +1,240 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here tomorrow is dropped resulting in 0 length event

Image

@@ -0,0 +1,240 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positives, But I think it's okay

Image

@@ -0,0 +1,240 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No support for overnight events
Image

Comment on lines +38 to +54
const chronoInstances = {
en: buildInstance(chrono.en),
de: buildInstance(chrono.de),
fr: buildInstance(chrono.fr),
es: buildInstance(chrono.es),
it: buildInstance(chrono.it),
nl: buildInstance(chrono.nl),
pt: buildInstance(chrono.pt),
ru: buildInstance(chrono.ru),
uk: buildInstance(chrono.uk),
sv: buildInstance(chrono.sv),
fi: buildInstance(chrono.fi),
ja: buildInstance(chrono.ja),
vi: buildInstance(chrono.vi),
'zh-hans': buildInstance(chrono.zh.hans),
'zh-hant': buildInstance(chrono.zh.hant),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better resolve lazily

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants