From 7b96ca66b6ce50aaab5c102bf1670d59fa74d2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Sun, 8 Mar 2026 21:22:57 -0400 Subject: [PATCH 01/11] Split leap year and weekday check intents --- __init__.py | 161 +++++++++++++++++- .../en-US/dialog/leap.year.current.no.dialog | 1 + .../en-US/dialog/leap.year.current.yes.dialog | 1 + .../en-US/dialog/leap.year.either.no.dialog | 1 + .../en-US/dialog/leap.year.either.yes.dialog | 1 + locale/en-US/dialog/leap.year.next.no.dialog | 1 + locale/en-US/dialog/leap.year.next.yes.dialog | 1 + .../weekday.matches.date.future.no.dialog | 1 + .../weekday.matches.date.future.yes.dialog | 1 + .../weekday.matches.date.past.no.dialog | 1 + .../weekday.matches.date.past.yes.dialog | 1 + locale/en-US/intents/is.leap.year.intent | 4 + locale/en-US/intents/next.leap.year.intent | 6 +- locale/en-US/intents/weekday.for.date.intent | 30 +--- .../en-US/intents/weekday.matches.date.intent | 42 +++++ .../fr-FR/dialog/leap.year.current.no.dialog | 1 + .../fr-FR/dialog/leap.year.current.yes.dialog | 1 + .../fr-FR/dialog/leap.year.either.no.dialog | 1 + .../fr-FR/dialog/leap.year.either.yes.dialog | 1 + locale/fr-FR/dialog/leap.year.next.no.dialog | 1 + locale/fr-FR/dialog/leap.year.next.yes.dialog | 1 + .../weekday.matches.date.future.no.dialog | 1 + .../weekday.matches.date.future.yes.dialog | 1 + .../weekday.matches.date.past.no.dialog | 1 + .../weekday.matches.date.past.yes.dialog | 1 + locale/fr-FR/intents/is.leap.year.intent | 8 + .../fr-FR/intents/weekday.matches.date.intent | 14 ++ translations/en-us/dialogs.json | 32 +++- translations/en-us/intents.json | 14 +- translations/fr-fr/dialogs.json | 30 ++++ translations/fr-fr/intents.json | 26 +++ 31 files changed, 347 insertions(+), 40 deletions(-) create mode 100644 locale/en-US/dialog/leap.year.current.no.dialog create mode 100644 locale/en-US/dialog/leap.year.current.yes.dialog create mode 100644 locale/en-US/dialog/leap.year.either.no.dialog create mode 100644 locale/en-US/dialog/leap.year.either.yes.dialog create mode 100644 locale/en-US/dialog/leap.year.next.no.dialog create mode 100644 locale/en-US/dialog/leap.year.next.yes.dialog create mode 100644 locale/en-US/dialog/weekday.matches.date.future.no.dialog create mode 100644 locale/en-US/dialog/weekday.matches.date.future.yes.dialog create mode 100644 locale/en-US/dialog/weekday.matches.date.past.no.dialog create mode 100644 locale/en-US/dialog/weekday.matches.date.past.yes.dialog create mode 100644 locale/en-US/intents/is.leap.year.intent create mode 100644 locale/en-US/intents/weekday.matches.date.intent create mode 100644 locale/fr-FR/dialog/leap.year.current.no.dialog create mode 100644 locale/fr-FR/dialog/leap.year.current.yes.dialog create mode 100644 locale/fr-FR/dialog/leap.year.either.no.dialog create mode 100644 locale/fr-FR/dialog/leap.year.either.yes.dialog create mode 100644 locale/fr-FR/dialog/leap.year.next.no.dialog create mode 100644 locale/fr-FR/dialog/leap.year.next.yes.dialog create mode 100644 locale/fr-FR/dialog/weekday.matches.date.future.no.dialog create mode 100644 locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog create mode 100644 locale/fr-FR/dialog/weekday.matches.date.past.no.dialog create mode 100644 locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog create mode 100644 locale/fr-FR/intents/is.leap.year.intent create mode 100644 locale/fr-FR/intents/weekday.matches.date.intent diff --git a/__init__.py b/__init__.py index 3b79933e..89582cb5 100644 --- a/__init__.py +++ b/__init__.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import calendar import datetime import os import re @@ -216,6 +217,91 @@ def _mentions_current_weekend(self, utterance: str) -> bool: normalized_utterance = utterance.casefold() return any(phrase in normalized_utterance for phrase in current_weekend_phrases) + def _extract_requested_weekday(self, utt: str): + """Extract the weekday requested by yes/no weekday-check intents.""" + language = self.lang.split("-")[0].lower() + weekday_map = { + "en": { + "monday": 0, + "tuesday": 1, + "wednesday": 2, + "thursday": 3, + "friday": 4, + "saturday": 5, + "sunday": 6, + }, + "fr": { + "lundi": 0, + "mardi": 1, + "mercredi": 2, + "jeudi": 3, + "vendredi": 4, + "samedi": 5, + "dimanche": 6, + } + }.get(language, {}) + if not utt or not weekday_map: + return None + + names = "|".join(map(re.escape, weekday_map)) + patterns = { + "en": [ + rf"\b(?:is|was)\b.*?\b(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", + rf"\bdoes\b.*?\b(?Pfall\s+(?:on\s+)?(?:a|an)\s+(?P{names}))\b", + rf"\bwill\b.*?\bbe\s+(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", + ], + "fr": [ + rf"\b(?Ptombe(?:-t-il)?\s+un\s+(?P{names}))\b", + ] + }.get(language, []) + + normalized = utt.lower() + for pattern in patterns: + match = re.search(pattern, normalized) + if match: + weekday = match.group("weekday") + return weekday_map[weekday], weekday, match.group("phrase") + return None + + def _strip_requested_weekday_phrase(self, utt: str, weekday) -> str: + """Remove the weekday-check wording before date parsing.""" + if not utt or weekday is None: + return utt + + _, _, phrase = weekday + cleaned = re.sub(re.escape(phrase), "", utt, flags=re.IGNORECASE) + if self.lang.split("-")[0].lower() == "en": + cleaned = re.sub(r"^\s*(?:does|is|was|will)\s+", "", cleaned, + flags=re.IGNORECASE) + cleaned = re.sub(r"\b(?:fall|be)\s*$", "", cleaned, + flags=re.IGNORECASE) + return cleaned.strip(" \t,;:.!?") + + def _get_leap_year_query_scope(self, utterance: str) -> str: + """Return whether the user asked about the current, next, or either year.""" + language = self.lang.split("-")[0].lower() + utterance = utterance.lower() + + either_phrases = { + "en": ("or next", "or the next one"), + "fr": ( + "ou la prochaine", + "ou l'année prochaine", + "ou l'an prochain", + "ou la suivante", + ) + }.get(language, ()) + next_year_phrases = { + "en": ("next year",), + "fr": ("année prochaine", "an prochain") + }.get(language, ()) + + if any(phrase in utterance for phrase in either_phrases): + return "either" + if any(phrase in utterance for phrase in next_year_phrases): + return "next" + return "current" + @staticmethod def _get_timezone_from_builtins(location_string: str) -> Optional[datetime.tzinfo]: """Attempt to resolve a timezone from a location name using geocoding. @@ -572,10 +658,46 @@ def handle_weekday(self, message): dialog = "weekday.at.date.past" # TODO - "today" should never trigger this intent, but if it does, # should we handle it better? nice_date will return "today" in that case - self.speak_dialog(dialog, { + self.speak_dialog(dialog, { "date": nice_date(dt, lang=self.lang, now=now), "weekday": nice_weekday(dt, lang=self.lang)}) + @intent_handler("weekday.matches.date.intent") + def handle_weekday_match(self, message): + """Handle yes/no questions about whether a date matches a weekday.""" + now = self.get_datetime() # session aware + utterance = message.data.get("utterance", "") + weekday = self._extract_requested_weekday(utterance) + date_text = message.data.get("date") + if not date_text: + date_text = self._strip_requested_weekday_phrase(utterance, weekday) + + dt, _ = extract_datetime(date_text or utterance, + anchorDate=now, lang=self.lang) or (None, None) + if not dt or weekday is None: + self.speak_dialog("extract.date.error") + return + + expected_weekday, expected_name, _ = weekday + data = { + "date": nice_date(dt, lang=self.lang, now=now), + "weekday": expected_name, + "actual_weekday": nice_weekday(dt, lang=self.lang) + } + if dt.date() >= now.date(): + dialog = ( + "weekday.matches.date.future.yes" + if dt.weekday() == expected_weekday + else "weekday.matches.date.future.no" + ) + else: + dialog = ( + "weekday.matches.date.past.yes" + if dt.weekday() == expected_weekday + else "weekday.matches.date.past.no" + ) + self.speak_dialog(dialog, data) + @intent_handler("what.month.is.it.intent") def handle_current_month(self, message): """ @@ -660,6 +782,43 @@ def handle_query_next_leap_year(self, message): next_leap_year = get_next_leap_year(year) self.speak_dialog('next.leap.year', {'year': next_leap_year}) + @intent_handler("is.leap.year.intent") + def handle_is_leap_year(self, message): + """Handle yes/no questions about leap years.""" + utterance = message.data.get("utterance", "") + now = self.get_datetime() + current_year = now.year + next_year = current_year + 1 + scope = self._get_leap_year_query_scope(utterance) + + if scope == "either": + if calendar.isleap(current_year): + self.speak_dialog("leap.year.either.yes", + {"year": current_year}) + elif calendar.isleap(next_year): + self.speak_dialog("leap.year.either.yes", + {"year": next_year}) + else: + self.speak_dialog("leap.year.either.no", + {"year": get_next_leap_year(next_year + 1)}) + return + + if scope == "next": + dialog = ( + "leap.year.next.yes" + if calendar.isleap(next_year) + else "leap.year.next.no" + ) + self.speak_dialog(dialog, {"year": next_year}) + return + + dialog = ( + "leap.year.current.yes" + if calendar.isleap(current_year) + else "leap.year.current.no" + ) + self.speak_dialog(dialog, {"year": current_year}) + ###################################################################### # GUI / Faceplate def show_date(self, dt: datetime.datetime, location: str): diff --git a/locale/en-US/dialog/leap.year.current.no.dialog b/locale/en-US/dialog/leap.year.current.no.dialog new file mode 100644 index 00000000..e7e5e501 --- /dev/null +++ b/locale/en-US/dialog/leap.year.current.no.dialog @@ -0,0 +1 @@ +No, {year} is not a leap year. diff --git a/locale/en-US/dialog/leap.year.current.yes.dialog b/locale/en-US/dialog/leap.year.current.yes.dialog new file mode 100644 index 00000000..8c157e47 --- /dev/null +++ b/locale/en-US/dialog/leap.year.current.yes.dialog @@ -0,0 +1 @@ +Yes, {year} is a leap year. diff --git a/locale/en-US/dialog/leap.year.either.no.dialog b/locale/en-US/dialog/leap.year.either.no.dialog new file mode 100644 index 00000000..a26eac77 --- /dev/null +++ b/locale/en-US/dialog/leap.year.either.no.dialog @@ -0,0 +1 @@ +No. The next leap year after that will be {year}. diff --git a/locale/en-US/dialog/leap.year.either.yes.dialog b/locale/en-US/dialog/leap.year.either.yes.dialog new file mode 100644 index 00000000..8c157e47 --- /dev/null +++ b/locale/en-US/dialog/leap.year.either.yes.dialog @@ -0,0 +1 @@ +Yes, {year} is a leap year. diff --git a/locale/en-US/dialog/leap.year.next.no.dialog b/locale/en-US/dialog/leap.year.next.no.dialog new file mode 100644 index 00000000..0f49055e --- /dev/null +++ b/locale/en-US/dialog/leap.year.next.no.dialog @@ -0,0 +1 @@ +No, {year} will not be a leap year. diff --git a/locale/en-US/dialog/leap.year.next.yes.dialog b/locale/en-US/dialog/leap.year.next.yes.dialog new file mode 100644 index 00000000..5fa6d854 --- /dev/null +++ b/locale/en-US/dialog/leap.year.next.yes.dialog @@ -0,0 +1 @@ +Yes, {year} will be a leap year. diff --git a/locale/en-US/dialog/weekday.matches.date.future.no.dialog b/locale/en-US/dialog/weekday.matches.date.future.no.dialog new file mode 100644 index 00000000..b6180a6b --- /dev/null +++ b/locale/en-US/dialog/weekday.matches.date.future.no.dialog @@ -0,0 +1 @@ +No, {date} will be {actual_weekday}, not {weekday}. diff --git a/locale/en-US/dialog/weekday.matches.date.future.yes.dialog b/locale/en-US/dialog/weekday.matches.date.future.yes.dialog new file mode 100644 index 00000000..bf5fbc05 --- /dev/null +++ b/locale/en-US/dialog/weekday.matches.date.future.yes.dialog @@ -0,0 +1 @@ +Yes, {date} will be {weekday}. diff --git a/locale/en-US/dialog/weekday.matches.date.past.no.dialog b/locale/en-US/dialog/weekday.matches.date.past.no.dialog new file mode 100644 index 00000000..c9594c1d --- /dev/null +++ b/locale/en-US/dialog/weekday.matches.date.past.no.dialog @@ -0,0 +1 @@ +No, {date} was {actual_weekday}, not {weekday}. diff --git a/locale/en-US/dialog/weekday.matches.date.past.yes.dialog b/locale/en-US/dialog/weekday.matches.date.past.yes.dialog new file mode 100644 index 00000000..cbc0302e --- /dev/null +++ b/locale/en-US/dialog/weekday.matches.date.past.yes.dialog @@ -0,0 +1 @@ +Yes, {date} was {weekday}. diff --git a/locale/en-US/intents/is.leap.year.intent b/locale/en-US/intents/is.leap.year.intent new file mode 100644 index 00000000..33f0ee23 --- /dev/null +++ b/locale/en-US/intents/is.leap.year.intent @@ -0,0 +1,4 @@ +is next year a leap year +is this year a leap year +is this year a leap year or next +is this year a leap year or the next one diff --git a/locale/en-US/intents/next.leap.year.intent b/locale/en-US/intents/next.leap.year.intent index dad67101..27d6fcbd 100644 --- a/locale/en-US/intents/next.leap.year.intent +++ b/locale/en-US/intents/next.leap.year.intent @@ -1,9 +1,5 @@ can you tell me when next leap year is can you tell me when the next leap year is -is next year a leap year -is this year a leap year -is this year a leap year or next -is this year a leap year or the next one tell me when next leap year is tell me when the next leap year is what date is the next leap year @@ -41,4 +37,4 @@ when will we have leap year again when will we have a leap year again when will we see leap year again when will we see a leap year again -which year is the next leap year \ No newline at end of file +which year is the next leap year diff --git a/locale/en-US/intents/weekday.for.date.intent b/locale/en-US/intents/weekday.for.date.intent index bf769860..02b16696 100644 --- a/locale/en-US/intents/weekday.for.date.intent +++ b/locale/en-US/intents/weekday.for.date.intent @@ -32,20 +32,6 @@ do you know which day {date} is do you know which day {date} was do you know which weekday {date} is do you know which weekday {date} was -is {date} a Friday -is {date} a Monday -is {date} a Saturday -is {date} a Sunday -is {date} a Thursday -is {date} a Tuesday -is {date} a Wednesday -is {date} on a Friday -is {date} on a Monday -is {date} on a Saturday -is {date} on a Sunday -is {date} on a Thursday -is {date} on a Tuesday -is {date} on a Wednesday on what day does {date} fall on on what day is {date} on what day of the week does {date} fall on @@ -78,20 +64,6 @@ tell me what day {date} is tell me what day {date} was tell me what weekday {date} is tell me what weekday {date} was -was {date} a Friday -was {date} a Monday -was {date} a Saturday -was {date} a Sunday -was {date} a Thursday -was {date} a Tuesday -was {date} a Wednesday -was {date} on a Friday -was {date} on a Monday -was {date} on a Saturday -was {date} on a Sunday -was {date} on a Thursday -was {date} on a Tuesday -was {date} on a Wednesday what day does {date} fall on what day is {date} what day of the week does {date} fall on @@ -129,4 +101,4 @@ which weekday is {date} which weekday was {date} which weekday will it be on {date} which weekday {date} is -which weekday {date} was \ No newline at end of file +which weekday {date} was diff --git a/locale/en-US/intents/weekday.matches.date.intent b/locale/en-US/intents/weekday.matches.date.intent new file mode 100644 index 00000000..5ccc35f2 --- /dev/null +++ b/locale/en-US/intents/weekday.matches.date.intent @@ -0,0 +1,42 @@ +does {date} fall on a Friday +does {date} fall on a Monday +does {date} fall on a Saturday +does {date} fall on a Sunday +does {date} fall on a Thursday +does {date} fall on a Tuesday +does {date} fall on a Wednesday +is {date} a Friday +is {date} a Monday +is {date} a Saturday +is {date} a Sunday +is {date} a Thursday +is {date} a Tuesday +is {date} a Wednesday +is {date} on a Friday +is {date} on a Monday +is {date} on a Saturday +is {date} on a Sunday +is {date} on a Thursday +is {date} on a Tuesday +is {date} on a Wednesday +was {date} a Friday +was {date} a Monday +was {date} a Saturday +was {date} a Sunday +was {date} a Thursday +was {date} a Tuesday +was {date} a Wednesday +was {date} on a Friday +was {date} on a Monday +was {date} on a Saturday +was {date} on a Sunday +was {date} on a Thursday +was {date} on a Tuesday +was {date} on a Wednesday +will {date} be on a Friday +will {date} be on a Monday +will {date} be on a Saturday +will {date} be on a Sunday +will {date} be on a Thursday +will {date} be on a Tuesday +will {date} be on a Wednesday diff --git a/locale/fr-FR/dialog/leap.year.current.no.dialog b/locale/fr-FR/dialog/leap.year.current.no.dialog new file mode 100644 index 00000000..67f4fee8 --- /dev/null +++ b/locale/fr-FR/dialog/leap.year.current.no.dialog @@ -0,0 +1 @@ +Non, {year} n'est pas une année bissextile. diff --git a/locale/fr-FR/dialog/leap.year.current.yes.dialog b/locale/fr-FR/dialog/leap.year.current.yes.dialog new file mode 100644 index 00000000..50d469a8 --- /dev/null +++ b/locale/fr-FR/dialog/leap.year.current.yes.dialog @@ -0,0 +1 @@ +Oui, {year} est une année bissextile. diff --git a/locale/fr-FR/dialog/leap.year.either.no.dialog b/locale/fr-FR/dialog/leap.year.either.no.dialog new file mode 100644 index 00000000..37c113e6 --- /dev/null +++ b/locale/fr-FR/dialog/leap.year.either.no.dialog @@ -0,0 +1 @@ +Non. La prochaine année bissextile après cela sera {year}. diff --git a/locale/fr-FR/dialog/leap.year.either.yes.dialog b/locale/fr-FR/dialog/leap.year.either.yes.dialog new file mode 100644 index 00000000..50d469a8 --- /dev/null +++ b/locale/fr-FR/dialog/leap.year.either.yes.dialog @@ -0,0 +1 @@ +Oui, {year} est une année bissextile. diff --git a/locale/fr-FR/dialog/leap.year.next.no.dialog b/locale/fr-FR/dialog/leap.year.next.no.dialog new file mode 100644 index 00000000..0ac7365d --- /dev/null +++ b/locale/fr-FR/dialog/leap.year.next.no.dialog @@ -0,0 +1 @@ +Non, {year} ne sera pas une année bissextile. diff --git a/locale/fr-FR/dialog/leap.year.next.yes.dialog b/locale/fr-FR/dialog/leap.year.next.yes.dialog new file mode 100644 index 00000000..62860b18 --- /dev/null +++ b/locale/fr-FR/dialog/leap.year.next.yes.dialog @@ -0,0 +1 @@ +Oui, {year} sera une année bissextile. diff --git a/locale/fr-FR/dialog/weekday.matches.date.future.no.dialog b/locale/fr-FR/dialog/weekday.matches.date.future.no.dialog new file mode 100644 index 00000000..699042cf --- /dev/null +++ b/locale/fr-FR/dialog/weekday.matches.date.future.no.dialog @@ -0,0 +1 @@ +Non, le {date} tombera un {actual_weekday}, pas un {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog b/locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog new file mode 100644 index 00000000..7bdfe5bb --- /dev/null +++ b/locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog @@ -0,0 +1 @@ +Oui, le {date} tombera un {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.past.no.dialog b/locale/fr-FR/dialog/weekday.matches.date.past.no.dialog new file mode 100644 index 00000000..671d0bd4 --- /dev/null +++ b/locale/fr-FR/dialog/weekday.matches.date.past.no.dialog @@ -0,0 +1 @@ +Non, le {date} tombait un {actual_weekday}, pas un {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog b/locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog new file mode 100644 index 00000000..6d93e6f7 --- /dev/null +++ b/locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog @@ -0,0 +1 @@ +Oui, le {date} tombait bien un {weekday}. diff --git a/locale/fr-FR/intents/is.leap.year.intent b/locale/fr-FR/intents/is.leap.year.intent new file mode 100644 index 00000000..167f8b97 --- /dev/null +++ b/locale/fr-FR/intents/is.leap.year.intent @@ -0,0 +1,8 @@ +cette année est-elle bissextile +cette année est-elle bissextile ou l'année prochaine +cette année est-elle bissextile ou la prochaine +est-ce que cette année est bissextile +est-ce que cette année est bissextile ou l'année prochaine +est-ce que cette année est bissextile ou la prochaine +est-ce que l'année prochaine est bissextile +l'année prochaine est-elle bissextile diff --git a/locale/fr-FR/intents/weekday.matches.date.intent b/locale/fr-FR/intents/weekday.matches.date.intent new file mode 100644 index 00000000..f2f9bda9 --- /dev/null +++ b/locale/fr-FR/intents/weekday.matches.date.intent @@ -0,0 +1,14 @@ +est-ce que {date} tombe un dimanche +est-ce que {date} tombe un jeudi +est-ce que {date} tombe un lundi +est-ce que {date} tombe un mardi +est-ce que {date} tombe un mercredi +est-ce que {date} tombe un samedi +est-ce que {date} tombe un vendredi +le {date} tombe-t-il un dimanche +le {date} tombe-t-il un jeudi +le {date} tombe-t-il un lundi +le {date} tombe-t-il un mardi +le {date} tombe-t-il un mercredi +le {date} tombe-t-il un samedi +le {date} tombe-t-il un vendredi diff --git a/translations/en-us/dialogs.json b/translations/en-us/dialogs.json index a7d7b6d6..14241605 100644 --- a/translations/en-us/dialogs.json +++ b/translations/en-us/dialogs.json @@ -79,6 +79,36 @@ "This year is {year}", "The current year is {year}", "We're in {year} right now" + ], + "/dialog/leap.year.current.no.dialog": [ + "No, {year} is not a leap year." + ], + "/dialog/leap.year.current.yes.dialog": [ + "Yes, {year} is a leap year." + ], + "/dialog/leap.year.either.no.dialog": [ + "No. The next leap year after that will be {year}." + ], + "/dialog/leap.year.either.yes.dialog": [ + "Yes, {year} is a leap year." + ], + "/dialog/leap.year.next.no.dialog": [ + "No, {year} will not be a leap year." + ], + "/dialog/leap.year.next.yes.dialog": [ + "Yes, {year} will be a leap year." + ], + "/dialog/weekday.matches.date.future.no.dialog": [ + "No, {date} will be {actual_weekday}, not {weekday}." + ], + "/dialog/weekday.matches.date.future.yes.dialog": [ + "Yes, {date} will be {weekday}." + ], + "/dialog/weekday.matches.date.past.no.dialog": [ + "No, {date} was {actual_weekday}, not {weekday}." + ], + "/dialog/weekday.matches.date.past.yes.dialog": [ + "Yes, {date} was {weekday}." ] -} \ No newline at end of file +} diff --git a/translations/en-us/intents.json b/translations/en-us/intents.json index c8e9c105..246fdbdf 100644 --- a/translations/en-us/intents.json +++ b/translations/en-us/intents.json @@ -9,10 +9,12 @@ "when will we (have|get|see) [a] leap year again", "when is (February 29th|Feb 29) happening (again|next)", "(what|when) is the next year that has (February 29th|366 days|an extra day [in February])", - "is (next year|this year) a leap year", - "is this year a leap year or (next|the next one)", "(what|which) year is the next leap year" ], + "/intents/is.leap.year.intent": [ + "is (next year|this year) a leap year", + "is this year a leap year or (next|the next one)" + ], "/intents/time.until.intent": [ "how (long|much time|much longer) (is left|do we have) (before|until) {date}", "how far (away|off) is {date} [from (today|now)]", @@ -59,13 +61,17 @@ "(which|what) (day of the week|weekday) (was|is) {date}", "(do you know|[can you] tell me) the (day of the week|weekday) for {date}", "what’s the (day of the week|weekday) for {date}", - "(was|is) {date} [on] a (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)", "[on] (what|which) (day|day of the week|weekday) does {date} fall on", "[on] (what|which) (day|day of the week|weekday) (was|will it be on) {date}", "[on] (what|which) (day|day of the week|weekday) (was|is) {date}", "[can you] tell me what (day|day of the week|weekday) {date} (was|is)", "[(I want to|do you) know] (which|what) (day|day of the week|weekday) {date} (was|is)" ], + "/intents/weekday.matches.date.intent": [ + "(was|is) {date} [on] a (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)", + "does {date} fall on a (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)", + "will {date} be on a (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)" + ], "/intents/what.weekday.is.it.intent": [ "(which|what) (day of the week|weekday) is (it|today)", "(do you know|[can you] tell me) the (day of the week|weekday)", @@ -132,4 +138,4 @@ "is it already (noon|midnight|[one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve]) [in {location}]", "is it already (morning|afternoon|evening|night) [in {location}]" ] -} \ No newline at end of file +} diff --git a/translations/fr-fr/dialogs.json b/translations/fr-fr/dialogs.json index 529ea4e3..e1a3c47f 100644 --- a/translations/fr-fr/dialogs.json +++ b/translations/fr-fr/dialogs.json @@ -36,6 +36,24 @@ "/dialog/extract.date.error.dialog": [ "Je n'ai pas compris \u00e0 quelle date vous faites r\u00e9f\u00e9rence." ], + "/dialog/leap.year.current.no.dialog": [ + "Non, {year} n'est pas une ann\u00e9e bissextile." + ], + "/dialog/leap.year.current.yes.dialog": [ + "Oui, {year} est une ann\u00e9e bissextile." + ], + "/dialog/leap.year.either.no.dialog": [ + "Non. La prochaine ann\u00e9e bissextile apr\u00e8s cela sera {year}." + ], + "/dialog/leap.year.either.yes.dialog": [ + "Oui, {year} est une ann\u00e9e bissextile." + ], + "/dialog/leap.year.next.no.dialog": [ + "Non, {year} ne sera pas une ann\u00e9e bissextile." + ], + "/dialog/leap.year.next.yes.dialog": [ + "Oui, {year} sera une ann\u00e9e bissextile." + ], "/dialog/month.current.dialog": [ "Le mois actuel est {month}", "Le mois en cours est {month}", @@ -71,6 +89,18 @@ "Nous sommes {weekday}", "C'est {weekday}" ], + "/dialog/weekday.matches.date.future.no.dialog": [ + "Non, le {date} tombera un {actual_weekday}, pas un {weekday}." + ], + "/dialog/weekday.matches.date.future.yes.dialog": [ + "Oui, le {date} tombera un {weekday}." + ], + "/dialog/weekday.matches.date.past.no.dialog": [ + "Non, le {date} tombait un {actual_weekday}, pas un {weekday}." + ], + "/dialog/weekday.matches.date.past.yes.dialog": [ + "Oui, le {date} tombait bien un {weekday}." + ], "/dialog/year.current.dialog": [ "L'ann\u00e9e actuelle est {year}", "L'ann\u00e9e en cours est {year}", diff --git a/translations/fr-fr/intents.json b/translations/fr-fr/intents.json index c12b69f9..ceda0b91 100644 --- a/translations/fr-fr/intents.json +++ b/translations/fr-fr/intents.json @@ -17,6 +17,32 @@ "dis-moi les dates du week-end dernier", "peux-tu me dire quand \u00e9tait le week-end dernier" ], + "/intents/is.leap.year.intent": [ + "cette année est-elle bissextile", + "cette année est-elle bissextile ou l'année prochaine", + "cette année est-elle bissextile ou la prochaine", + "est-ce que cette année est bissextile", + "est-ce que cette année est bissextile ou l'année prochaine", + "est-ce que cette année est bissextile ou la prochaine", + "est-ce que l'année prochaine est bissextile", + "l'année prochaine est-elle bissextile" + ], + "/intents/weekday.matches.date.intent": [ + "est-ce que {date} tombe un dimanche", + "est-ce que {date} tombe un jeudi", + "est-ce que {date} tombe un lundi", + "est-ce que {date} tombe un mardi", + "est-ce que {date} tombe un mercredi", + "est-ce que {date} tombe un samedi", + "est-ce que {date} tombe un vendredi", + "le {date} tombe-t-il un dimanche", + "le {date} tombe-t-il un jeudi", + "le {date} tombe-t-il un lundi", + "le {date} tombe-t-il un mardi", + "le {date} tombe-t-il un mercredi", + "le {date} tombe-t-il un samedi", + "le {date} tombe-t-il un vendredi" + ], "/intents/what.time.is.it.intent": [ "quelle heure est-il", "quelle heure est-il \u00e0 {location}", From ec7b54ef5a9f2446b622d0f7845077f884c6b984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Mon, 9 Mar 2026 06:51:34 -0400 Subject: [PATCH 02/11] Fix weekday match review feedback --- __init__.py | 10 ++++++++-- .../dialog/weekday.matches.date.today.no.dialog | 1 + .../dialog/weekday.matches.date.today.yes.dialog | 1 + .../dialog/weekday.matches.date.future.no.dialog | 2 +- .../dialog/weekday.matches.date.future.yes.dialog | 2 +- .../dialog/weekday.matches.date.past.no.dialog | 2 +- .../dialog/weekday.matches.date.past.yes.dialog | 2 +- .../dialog/weekday.matches.date.today.no.dialog | 1 + .../dialog/weekday.matches.date.today.yes.dialog | 1 + locale/fr-FR/intents/is.leap.year.intent | 4 ++++ translations/en-us/dialogs.json | 6 ++++++ translations/fr-fr/dialogs.json | 14 ++++++++++---- translations/fr-fr/intents.json | 4 ++++ 13 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 locale/en-US/dialog/weekday.matches.date.today.no.dialog create mode 100644 locale/en-US/dialog/weekday.matches.date.today.yes.dialog create mode 100644 locale/fr-FR/dialog/weekday.matches.date.today.no.dialog create mode 100644 locale/fr-FR/dialog/weekday.matches.date.today.yes.dialog diff --git a/__init__.py b/__init__.py index 89582cb5..bef285d5 100644 --- a/__init__.py +++ b/__init__.py @@ -251,7 +251,7 @@ def _extract_requested_weekday(self, utt: str): rf"\bwill\b.*?\bbe\s+(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", ], "fr": [ - rf"\b(?Ptombe(?:-t-il)?\s+un\s+(?P{names}))\b", + rf"\b(?P(?:tombe(?:-t-il)?|tombait(?:-il)?|tombera(?:-t-il)?|tomberait(?:-il)?)\s+un\s+(?P{names}))\b", ] }.get(language, []) @@ -684,12 +684,18 @@ def handle_weekday_match(self, message): "weekday": expected_name, "actual_weekday": nice_weekday(dt, lang=self.lang) } - if dt.date() >= now.date(): + if dt.date() > now.date(): dialog = ( "weekday.matches.date.future.yes" if dt.weekday() == expected_weekday else "weekday.matches.date.future.no" ) + elif dt.date() == now.date(): + dialog = ( + "weekday.matches.date.today.yes" + if dt.weekday() == expected_weekday + else "weekday.matches.date.today.no" + ) else: dialog = ( "weekday.matches.date.past.yes" diff --git a/locale/en-US/dialog/weekday.matches.date.today.no.dialog b/locale/en-US/dialog/weekday.matches.date.today.no.dialog new file mode 100644 index 00000000..55c3620e --- /dev/null +++ b/locale/en-US/dialog/weekday.matches.date.today.no.dialog @@ -0,0 +1 @@ +No, {date} is {actual_weekday}, not {weekday}. diff --git a/locale/en-US/dialog/weekday.matches.date.today.yes.dialog b/locale/en-US/dialog/weekday.matches.date.today.yes.dialog new file mode 100644 index 00000000..d4a95fc4 --- /dev/null +++ b/locale/en-US/dialog/weekday.matches.date.today.yes.dialog @@ -0,0 +1 @@ +Yes, {date} is {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.future.no.dialog b/locale/fr-FR/dialog/weekday.matches.date.future.no.dialog index 699042cf..41dc8d75 100644 --- a/locale/fr-FR/dialog/weekday.matches.date.future.no.dialog +++ b/locale/fr-FR/dialog/weekday.matches.date.future.no.dialog @@ -1 +1 @@ -Non, le {date} tombera un {actual_weekday}, pas un {weekday}. +Non, {date}, ce sera {actual_weekday}, pas {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog b/locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog index 7bdfe5bb..b97b3972 100644 --- a/locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog +++ b/locale/fr-FR/dialog/weekday.matches.date.future.yes.dialog @@ -1 +1 @@ -Oui, le {date} tombera un {weekday}. +Oui, {date}, ce sera {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.past.no.dialog b/locale/fr-FR/dialog/weekday.matches.date.past.no.dialog index 671d0bd4..a72c7179 100644 --- a/locale/fr-FR/dialog/weekday.matches.date.past.no.dialog +++ b/locale/fr-FR/dialog/weekday.matches.date.past.no.dialog @@ -1 +1 @@ -Non, le {date} tombait un {actual_weekday}, pas un {weekday}. +Non, {date}, c'était {actual_weekday}, pas {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog b/locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog index 6d93e6f7..0a826402 100644 --- a/locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog +++ b/locale/fr-FR/dialog/weekday.matches.date.past.yes.dialog @@ -1 +1 @@ -Oui, le {date} tombait bien un {weekday}. +Oui, {date}, c'était bien {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.today.no.dialog b/locale/fr-FR/dialog/weekday.matches.date.today.no.dialog new file mode 100644 index 00000000..7fdfbd33 --- /dev/null +++ b/locale/fr-FR/dialog/weekday.matches.date.today.no.dialog @@ -0,0 +1 @@ +Non, {date}, c'est {actual_weekday}, pas {weekday}. diff --git a/locale/fr-FR/dialog/weekday.matches.date.today.yes.dialog b/locale/fr-FR/dialog/weekday.matches.date.today.yes.dialog new file mode 100644 index 00000000..422ec1bd --- /dev/null +++ b/locale/fr-FR/dialog/weekday.matches.date.today.yes.dialog @@ -0,0 +1 @@ +Oui, {date}, c'est {weekday}. diff --git a/locale/fr-FR/intents/is.leap.year.intent b/locale/fr-FR/intents/is.leap.year.intent index 167f8b97..98950c8f 100644 --- a/locale/fr-FR/intents/is.leap.year.intent +++ b/locale/fr-FR/intents/is.leap.year.intent @@ -1,8 +1,12 @@ cette année est-elle bissextile +cette année est-elle bissextile ou l'an prochain cette année est-elle bissextile ou l'année prochaine cette année est-elle bissextile ou la prochaine est-ce que cette année est bissextile +est-ce que cette année est bissextile ou l'an prochain est-ce que cette année est bissextile ou l'année prochaine est-ce que cette année est bissextile ou la prochaine +est-ce que l'an prochain est bissextile est-ce que l'année prochaine est bissextile +l'an prochain est-il bissextile l'année prochaine est-elle bissextile diff --git a/translations/en-us/dialogs.json b/translations/en-us/dialogs.json index 14241605..fb8a78e2 100644 --- a/translations/en-us/dialogs.json +++ b/translations/en-us/dialogs.json @@ -109,6 +109,12 @@ ], "/dialog/weekday.matches.date.past.yes.dialog": [ "Yes, {date} was {weekday}." + ], + "/dialog/weekday.matches.date.today.no.dialog": [ + "No, {date} is {actual_weekday}, not {weekday}." + ], + "/dialog/weekday.matches.date.today.yes.dialog": [ + "Yes, {date} is {weekday}." ] } diff --git a/translations/fr-fr/dialogs.json b/translations/fr-fr/dialogs.json index e1a3c47f..b25eaec3 100644 --- a/translations/fr-fr/dialogs.json +++ b/translations/fr-fr/dialogs.json @@ -90,16 +90,22 @@ "C'est {weekday}" ], "/dialog/weekday.matches.date.future.no.dialog": [ - "Non, le {date} tombera un {actual_weekday}, pas un {weekday}." + "Non, {date}, ce sera {actual_weekday}, pas {weekday}." ], "/dialog/weekday.matches.date.future.yes.dialog": [ - "Oui, le {date} tombera un {weekday}." + "Oui, {date}, ce sera {weekday}." ], "/dialog/weekday.matches.date.past.no.dialog": [ - "Non, le {date} tombait un {actual_weekday}, pas un {weekday}." + "Non, {date}, c'était {actual_weekday}, pas {weekday}." ], "/dialog/weekday.matches.date.past.yes.dialog": [ - "Oui, le {date} tombait bien un {weekday}." + "Oui, {date}, c'était bien {weekday}." + ], + "/dialog/weekday.matches.date.today.no.dialog": [ + "Non, {date}, c'est {actual_weekday}, pas {weekday}." + ], + "/dialog/weekday.matches.date.today.yes.dialog": [ + "Oui, {date}, c'est {weekday}." ], "/dialog/year.current.dialog": [ "L'ann\u00e9e actuelle est {year}", diff --git a/translations/fr-fr/intents.json b/translations/fr-fr/intents.json index ceda0b91..934a2e88 100644 --- a/translations/fr-fr/intents.json +++ b/translations/fr-fr/intents.json @@ -19,12 +19,16 @@ ], "/intents/is.leap.year.intent": [ "cette année est-elle bissextile", + "cette année est-elle bissextile ou l'an prochain", "cette année est-elle bissextile ou l'année prochaine", "cette année est-elle bissextile ou la prochaine", "est-ce que cette année est bissextile", + "est-ce que cette année est bissextile ou l'an prochain", "est-ce que cette année est bissextile ou l'année prochaine", "est-ce que cette année est bissextile ou la prochaine", + "est-ce que l'an prochain est bissextile", "est-ce que l'année prochaine est bissextile", + "l'an prochain est-il bissextile", "l'année prochaine est-elle bissextile" ], "/intents/weekday.matches.date.intent": [ From d747d06c761683b2ba11ba944228712d5f73761e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Mon, 9 Mar 2026 06:55:05 -0400 Subject: [PATCH 03/11] Split date-time intent handler code into separate PR --- __init__.py | 167 +--------------------------------------------------- 1 file changed, 1 insertion(+), 166 deletions(-) diff --git a/__init__.py b/__init__.py index bef285d5..3b79933e 100644 --- a/__init__.py +++ b/__init__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import calendar import datetime import os import re @@ -217,91 +216,6 @@ def _mentions_current_weekend(self, utterance: str) -> bool: normalized_utterance = utterance.casefold() return any(phrase in normalized_utterance for phrase in current_weekend_phrases) - def _extract_requested_weekday(self, utt: str): - """Extract the weekday requested by yes/no weekday-check intents.""" - language = self.lang.split("-")[0].lower() - weekday_map = { - "en": { - "monday": 0, - "tuesday": 1, - "wednesday": 2, - "thursday": 3, - "friday": 4, - "saturday": 5, - "sunday": 6, - }, - "fr": { - "lundi": 0, - "mardi": 1, - "mercredi": 2, - "jeudi": 3, - "vendredi": 4, - "samedi": 5, - "dimanche": 6, - } - }.get(language, {}) - if not utt or not weekday_map: - return None - - names = "|".join(map(re.escape, weekday_map)) - patterns = { - "en": [ - rf"\b(?:is|was)\b.*?\b(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", - rf"\bdoes\b.*?\b(?Pfall\s+(?:on\s+)?(?:a|an)\s+(?P{names}))\b", - rf"\bwill\b.*?\bbe\s+(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", - ], - "fr": [ - rf"\b(?P(?:tombe(?:-t-il)?|tombait(?:-il)?|tombera(?:-t-il)?|tomberait(?:-il)?)\s+un\s+(?P{names}))\b", - ] - }.get(language, []) - - normalized = utt.lower() - for pattern in patterns: - match = re.search(pattern, normalized) - if match: - weekday = match.group("weekday") - return weekday_map[weekday], weekday, match.group("phrase") - return None - - def _strip_requested_weekday_phrase(self, utt: str, weekday) -> str: - """Remove the weekday-check wording before date parsing.""" - if not utt or weekday is None: - return utt - - _, _, phrase = weekday - cleaned = re.sub(re.escape(phrase), "", utt, flags=re.IGNORECASE) - if self.lang.split("-")[0].lower() == "en": - cleaned = re.sub(r"^\s*(?:does|is|was|will)\s+", "", cleaned, - flags=re.IGNORECASE) - cleaned = re.sub(r"\b(?:fall|be)\s*$", "", cleaned, - flags=re.IGNORECASE) - return cleaned.strip(" \t,;:.!?") - - def _get_leap_year_query_scope(self, utterance: str) -> str: - """Return whether the user asked about the current, next, or either year.""" - language = self.lang.split("-")[0].lower() - utterance = utterance.lower() - - either_phrases = { - "en": ("or next", "or the next one"), - "fr": ( - "ou la prochaine", - "ou l'année prochaine", - "ou l'an prochain", - "ou la suivante", - ) - }.get(language, ()) - next_year_phrases = { - "en": ("next year",), - "fr": ("année prochaine", "an prochain") - }.get(language, ()) - - if any(phrase in utterance for phrase in either_phrases): - return "either" - if any(phrase in utterance for phrase in next_year_phrases): - return "next" - return "current" - @staticmethod def _get_timezone_from_builtins(location_string: str) -> Optional[datetime.tzinfo]: """Attempt to resolve a timezone from a location name using geocoding. @@ -658,52 +572,10 @@ def handle_weekday(self, message): dialog = "weekday.at.date.past" # TODO - "today" should never trigger this intent, but if it does, # should we handle it better? nice_date will return "today" in that case - self.speak_dialog(dialog, { + self.speak_dialog(dialog, { "date": nice_date(dt, lang=self.lang, now=now), "weekday": nice_weekday(dt, lang=self.lang)}) - @intent_handler("weekday.matches.date.intent") - def handle_weekday_match(self, message): - """Handle yes/no questions about whether a date matches a weekday.""" - now = self.get_datetime() # session aware - utterance = message.data.get("utterance", "") - weekday = self._extract_requested_weekday(utterance) - date_text = message.data.get("date") - if not date_text: - date_text = self._strip_requested_weekday_phrase(utterance, weekday) - - dt, _ = extract_datetime(date_text or utterance, - anchorDate=now, lang=self.lang) or (None, None) - if not dt or weekday is None: - self.speak_dialog("extract.date.error") - return - - expected_weekday, expected_name, _ = weekday - data = { - "date": nice_date(dt, lang=self.lang, now=now), - "weekday": expected_name, - "actual_weekday": nice_weekday(dt, lang=self.lang) - } - if dt.date() > now.date(): - dialog = ( - "weekday.matches.date.future.yes" - if dt.weekday() == expected_weekday - else "weekday.matches.date.future.no" - ) - elif dt.date() == now.date(): - dialog = ( - "weekday.matches.date.today.yes" - if dt.weekday() == expected_weekday - else "weekday.matches.date.today.no" - ) - else: - dialog = ( - "weekday.matches.date.past.yes" - if dt.weekday() == expected_weekday - else "weekday.matches.date.past.no" - ) - self.speak_dialog(dialog, data) - @intent_handler("what.month.is.it.intent") def handle_current_month(self, message): """ @@ -788,43 +660,6 @@ def handle_query_next_leap_year(self, message): next_leap_year = get_next_leap_year(year) self.speak_dialog('next.leap.year', {'year': next_leap_year}) - @intent_handler("is.leap.year.intent") - def handle_is_leap_year(self, message): - """Handle yes/no questions about leap years.""" - utterance = message.data.get("utterance", "") - now = self.get_datetime() - current_year = now.year - next_year = current_year + 1 - scope = self._get_leap_year_query_scope(utterance) - - if scope == "either": - if calendar.isleap(current_year): - self.speak_dialog("leap.year.either.yes", - {"year": current_year}) - elif calendar.isleap(next_year): - self.speak_dialog("leap.year.either.yes", - {"year": next_year}) - else: - self.speak_dialog("leap.year.either.no", - {"year": get_next_leap_year(next_year + 1)}) - return - - if scope == "next": - dialog = ( - "leap.year.next.yes" - if calendar.isleap(next_year) - else "leap.year.next.no" - ) - self.speak_dialog(dialog, {"year": next_year}) - return - - dialog = ( - "leap.year.current.yes" - if calendar.isleap(current_year) - else "leap.year.current.no" - ) - self.speak_dialog(dialog, {"year": current_year}) - ###################################################################### # GUI / Faceplate def show_date(self, dt: datetime.datetime, location: str): From f05827a56b12335d945943eca55c067f13a8850e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Mon, 9 Mar 2026 07:53:59 -0400 Subject: [PATCH 04/11] Add locale parser resources for date checks --- locale/en-US/leap.year.scope.either_phrases.list | 2 ++ locale/en-US/leap.year.scope.next_phrases.list | 1 + locale/en-US/weekday.match.patterns.list | 3 +++ locale/en-US/weekday.match.strip_prefixes.list | 1 + locale/en-US/weekday.match.strip_suffixes.list | 1 + locale/en-US/weekday.match.weekdays.value | 7 +++++++ locale/fr-FR/leap.year.scope.either_phrases.list | 4 ++++ locale/fr-FR/leap.year.scope.next_phrases.list | 2 ++ locale/fr-FR/weekday.match.patterns.list | 1 + locale/fr-FR/weekday.match.weekdays.value | 7 +++++++ 10 files changed, 29 insertions(+) create mode 100644 locale/en-US/leap.year.scope.either_phrases.list create mode 100644 locale/en-US/leap.year.scope.next_phrases.list create mode 100644 locale/en-US/weekday.match.patterns.list create mode 100644 locale/en-US/weekday.match.strip_prefixes.list create mode 100644 locale/en-US/weekday.match.strip_suffixes.list create mode 100644 locale/en-US/weekday.match.weekdays.value create mode 100644 locale/fr-FR/leap.year.scope.either_phrases.list create mode 100644 locale/fr-FR/leap.year.scope.next_phrases.list create mode 100644 locale/fr-FR/weekday.match.patterns.list create mode 100644 locale/fr-FR/weekday.match.weekdays.value diff --git a/locale/en-US/leap.year.scope.either_phrases.list b/locale/en-US/leap.year.scope.either_phrases.list new file mode 100644 index 00000000..9cf09496 --- /dev/null +++ b/locale/en-US/leap.year.scope.either_phrases.list @@ -0,0 +1,2 @@ +or next +or the next one diff --git a/locale/en-US/leap.year.scope.next_phrases.list b/locale/en-US/leap.year.scope.next_phrases.list new file mode 100644 index 00000000..80e4aeaf --- /dev/null +++ b/locale/en-US/leap.year.scope.next_phrases.list @@ -0,0 +1 @@ +next year diff --git a/locale/en-US/weekday.match.patterns.list b/locale/en-US/weekday.match.patterns.list new file mode 100644 index 00000000..00743656 --- /dev/null +++ b/locale/en-US/weekday.match.patterns.list @@ -0,0 +1,3 @@ +\b(?:is|was)\b.*?\b(?P(?:on\s+)?(?:a|an)\s+(?P__WEEKDAY_NAMES__))\b +\bdoes\b.*?\b(?Pfall\s+(?:on\s+)?(?:a|an)\s+(?P__WEEKDAY_NAMES__))\b +\bwill\b.*?\bbe\s+(?P(?:on\s+)?(?:a|an)\s+(?P__WEEKDAY_NAMES__))\b diff --git a/locale/en-US/weekday.match.strip_prefixes.list b/locale/en-US/weekday.match.strip_prefixes.list new file mode 100644 index 00000000..38838919 --- /dev/null +++ b/locale/en-US/weekday.match.strip_prefixes.list @@ -0,0 +1 @@ +^\s*(?:does|is|was|will)\s+ diff --git a/locale/en-US/weekday.match.strip_suffixes.list b/locale/en-US/weekday.match.strip_suffixes.list new file mode 100644 index 00000000..300a8b14 --- /dev/null +++ b/locale/en-US/weekday.match.strip_suffixes.list @@ -0,0 +1 @@ +\b(?:fall|be)\s*$ diff --git a/locale/en-US/weekday.match.weekdays.value b/locale/en-US/weekday.match.weekdays.value new file mode 100644 index 00000000..2cd43286 --- /dev/null +++ b/locale/en-US/weekday.match.weekdays.value @@ -0,0 +1,7 @@ +monday,0 +tuesday,1 +wednesday,2 +thursday,3 +friday,4 +saturday,5 +sunday,6 diff --git a/locale/fr-FR/leap.year.scope.either_phrases.list b/locale/fr-FR/leap.year.scope.either_phrases.list new file mode 100644 index 00000000..2da07c09 --- /dev/null +++ b/locale/fr-FR/leap.year.scope.either_phrases.list @@ -0,0 +1,4 @@ +ou la prochaine +ou l'année prochaine +ou l'an prochain +ou la suivante diff --git a/locale/fr-FR/leap.year.scope.next_phrases.list b/locale/fr-FR/leap.year.scope.next_phrases.list new file mode 100644 index 00000000..d31a6e77 --- /dev/null +++ b/locale/fr-FR/leap.year.scope.next_phrases.list @@ -0,0 +1,2 @@ +année prochaine +an prochain diff --git a/locale/fr-FR/weekday.match.patterns.list b/locale/fr-FR/weekday.match.patterns.list new file mode 100644 index 00000000..e221a09f --- /dev/null +++ b/locale/fr-FR/weekday.match.patterns.list @@ -0,0 +1 @@ +\b(?P(?:tombe(?:-t-il)?|tombait(?:-il)?|tombera(?:-t-il)?|tomberait(?:-il)?)\s+un\s+(?P__WEEKDAY_NAMES__))\b diff --git a/locale/fr-FR/weekday.match.weekdays.value b/locale/fr-FR/weekday.match.weekdays.value new file mode 100644 index 00000000..8cbbe1e1 --- /dev/null +++ b/locale/fr-FR/weekday.match.weekdays.value @@ -0,0 +1,7 @@ +lundi,0 +mardi,1 +mercredi,2 +jeudi,3 +vendredi,4 +samedi,5 +dimanche,6 From e14dc96541b415a3dac0c81e93c55d5d9336aec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Mon, 9 Mar 2026 06:54:36 -0400 Subject: [PATCH 05/11] Split date-time intent handler code --- __init__.py | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 3b79933e..bef285d5 100644 --- a/__init__.py +++ b/__init__.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import calendar import datetime import os import re @@ -216,6 +217,91 @@ def _mentions_current_weekend(self, utterance: str) -> bool: normalized_utterance = utterance.casefold() return any(phrase in normalized_utterance for phrase in current_weekend_phrases) + def _extract_requested_weekday(self, utt: str): + """Extract the weekday requested by yes/no weekday-check intents.""" + language = self.lang.split("-")[0].lower() + weekday_map = { + "en": { + "monday": 0, + "tuesday": 1, + "wednesday": 2, + "thursday": 3, + "friday": 4, + "saturday": 5, + "sunday": 6, + }, + "fr": { + "lundi": 0, + "mardi": 1, + "mercredi": 2, + "jeudi": 3, + "vendredi": 4, + "samedi": 5, + "dimanche": 6, + } + }.get(language, {}) + if not utt or not weekday_map: + return None + + names = "|".join(map(re.escape, weekday_map)) + patterns = { + "en": [ + rf"\b(?:is|was)\b.*?\b(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", + rf"\bdoes\b.*?\b(?Pfall\s+(?:on\s+)?(?:a|an)\s+(?P{names}))\b", + rf"\bwill\b.*?\bbe\s+(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", + ], + "fr": [ + rf"\b(?P(?:tombe(?:-t-il)?|tombait(?:-il)?|tombera(?:-t-il)?|tomberait(?:-il)?)\s+un\s+(?P{names}))\b", + ] + }.get(language, []) + + normalized = utt.lower() + for pattern in patterns: + match = re.search(pattern, normalized) + if match: + weekday = match.group("weekday") + return weekday_map[weekday], weekday, match.group("phrase") + return None + + def _strip_requested_weekday_phrase(self, utt: str, weekday) -> str: + """Remove the weekday-check wording before date parsing.""" + if not utt or weekday is None: + return utt + + _, _, phrase = weekday + cleaned = re.sub(re.escape(phrase), "", utt, flags=re.IGNORECASE) + if self.lang.split("-")[0].lower() == "en": + cleaned = re.sub(r"^\s*(?:does|is|was|will)\s+", "", cleaned, + flags=re.IGNORECASE) + cleaned = re.sub(r"\b(?:fall|be)\s*$", "", cleaned, + flags=re.IGNORECASE) + return cleaned.strip(" \t,;:.!?") + + def _get_leap_year_query_scope(self, utterance: str) -> str: + """Return whether the user asked about the current, next, or either year.""" + language = self.lang.split("-")[0].lower() + utterance = utterance.lower() + + either_phrases = { + "en": ("or next", "or the next one"), + "fr": ( + "ou la prochaine", + "ou l'année prochaine", + "ou l'an prochain", + "ou la suivante", + ) + }.get(language, ()) + next_year_phrases = { + "en": ("next year",), + "fr": ("année prochaine", "an prochain") + }.get(language, ()) + + if any(phrase in utterance for phrase in either_phrases): + return "either" + if any(phrase in utterance for phrase in next_year_phrases): + return "next" + return "current" + @staticmethod def _get_timezone_from_builtins(location_string: str) -> Optional[datetime.tzinfo]: """Attempt to resolve a timezone from a location name using geocoding. @@ -572,10 +658,52 @@ def handle_weekday(self, message): dialog = "weekday.at.date.past" # TODO - "today" should never trigger this intent, but if it does, # should we handle it better? nice_date will return "today" in that case - self.speak_dialog(dialog, { + self.speak_dialog(dialog, { "date": nice_date(dt, lang=self.lang, now=now), "weekday": nice_weekday(dt, lang=self.lang)}) + @intent_handler("weekday.matches.date.intent") + def handle_weekday_match(self, message): + """Handle yes/no questions about whether a date matches a weekday.""" + now = self.get_datetime() # session aware + utterance = message.data.get("utterance", "") + weekday = self._extract_requested_weekday(utterance) + date_text = message.data.get("date") + if not date_text: + date_text = self._strip_requested_weekday_phrase(utterance, weekday) + + dt, _ = extract_datetime(date_text or utterance, + anchorDate=now, lang=self.lang) or (None, None) + if not dt or weekday is None: + self.speak_dialog("extract.date.error") + return + + expected_weekday, expected_name, _ = weekday + data = { + "date": nice_date(dt, lang=self.lang, now=now), + "weekday": expected_name, + "actual_weekday": nice_weekday(dt, lang=self.lang) + } + if dt.date() > now.date(): + dialog = ( + "weekday.matches.date.future.yes" + if dt.weekday() == expected_weekday + else "weekday.matches.date.future.no" + ) + elif dt.date() == now.date(): + dialog = ( + "weekday.matches.date.today.yes" + if dt.weekday() == expected_weekday + else "weekday.matches.date.today.no" + ) + else: + dialog = ( + "weekday.matches.date.past.yes" + if dt.weekday() == expected_weekday + else "weekday.matches.date.past.no" + ) + self.speak_dialog(dialog, data) + @intent_handler("what.month.is.it.intent") def handle_current_month(self, message): """ @@ -660,6 +788,43 @@ def handle_query_next_leap_year(self, message): next_leap_year = get_next_leap_year(year) self.speak_dialog('next.leap.year', {'year': next_leap_year}) + @intent_handler("is.leap.year.intent") + def handle_is_leap_year(self, message): + """Handle yes/no questions about leap years.""" + utterance = message.data.get("utterance", "") + now = self.get_datetime() + current_year = now.year + next_year = current_year + 1 + scope = self._get_leap_year_query_scope(utterance) + + if scope == "either": + if calendar.isleap(current_year): + self.speak_dialog("leap.year.either.yes", + {"year": current_year}) + elif calendar.isleap(next_year): + self.speak_dialog("leap.year.either.yes", + {"year": next_year}) + else: + self.speak_dialog("leap.year.either.no", + {"year": get_next_leap_year(next_year + 1)}) + return + + if scope == "next": + dialog = ( + "leap.year.next.yes" + if calendar.isleap(next_year) + else "leap.year.next.no" + ) + self.speak_dialog(dialog, {"year": next_year}) + return + + dialog = ( + "leap.year.current.yes" + if calendar.isleap(current_year) + else "leap.year.current.no" + ) + self.speak_dialog(dialog, {"year": current_year}) + ###################################################################### # GUI / Faceplate def show_date(self, dt: datetime.datetime, location: str): From 7fe7fc33edc727c784440a45b2773daad6b7592b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Mon, 9 Mar 2026 07:33:03 -0400 Subject: [PATCH 06/11] Guard weekday parsing failures --- __init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index bef285d5..044583b3 100644 --- a/__init__.py +++ b/__init__.py @@ -651,13 +651,14 @@ def handle_weekday(self, message): anchorDate=now, lang=self.lang) or (None, None) if not dt: self.speak_dialog("extract.date.error") + return + + if dt >= now: + dialog = "weekday.at.date.future" else: - if dt >= now: - dialog = "weekday.at.date.future" - else: - dialog = "weekday.at.date.past" - # TODO - "today" should never trigger this intent, but if it does, - # should we handle it better? nice_date will return "today" in that case + dialog = "weekday.at.date.past" + # TODO - "today" should never trigger this intent, but if it does, + # should we handle it better? nice_date will return "today" in that case self.speak_dialog(dialog, { "date": nice_date(dt, lang=self.lang, now=now), "weekday": nice_weekday(dt, lang=self.lang)}) From 8f112250e91c7a2e3ac507e429cbeb83ef1ea413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Mon, 9 Mar 2026 07:53:27 -0400 Subject: [PATCH 07/11] Load weekday parsing rules from resources --- __init__.py | 83 +++++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/__init__.py b/__init__.py index 044583b3..eae5dd26 100644 --- a/__init__.py +++ b/__init__.py @@ -217,43 +217,39 @@ def _mentions_current_weekend(self, utterance: str) -> bool: normalized_utterance = utterance.casefold() return any(phrase in normalized_utterance for phrase in current_weekend_phrases) + def _load_cached_list(self, name: str): + """Load and cache a locale list resource.""" + cache_key = f"{name}.list" + if cache_key not in self.resources.static: + self.resources.static[cache_key] = self.resources.load_list_file(name) + return self.resources.static[cache_key] + + def _load_weekday_match_map(self): + """Load the locale weekday map for weekday-match parsing.""" + cache_key = "weekday.match.weekdays.value" + if cache_key not in self.resources.static: + weekday_map = {} + for name, value in self.resources.load_named_value_file( + "weekday.match.weekdays" + ).items(): + try: + weekday_map[name.lower()] = int(value) + except (TypeError, ValueError): + continue + self.resources.static[cache_key] = weekday_map + return self.resources.static[cache_key] + def _extract_requested_weekday(self, utt: str): """Extract the weekday requested by yes/no weekday-check intents.""" - language = self.lang.split("-")[0].lower() - weekday_map = { - "en": { - "monday": 0, - "tuesday": 1, - "wednesday": 2, - "thursday": 3, - "friday": 4, - "saturday": 5, - "sunday": 6, - }, - "fr": { - "lundi": 0, - "mardi": 1, - "mercredi": 2, - "jeudi": 3, - "vendredi": 4, - "samedi": 5, - "dimanche": 6, - } - }.get(language, {}) + weekday_map = self._load_weekday_match_map() if not utt or not weekday_map: return None names = "|".join(map(re.escape, weekday_map)) - patterns = { - "en": [ - rf"\b(?:is|was)\b.*?\b(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", - rf"\bdoes\b.*?\b(?Pfall\s+(?:on\s+)?(?:a|an)\s+(?P{names}))\b", - rf"\bwill\b.*?\bbe\s+(?P(?:on\s+)?(?:a|an)\s+(?P{names}))\b", - ], - "fr": [ - rf"\b(?P(?:tombe(?:-t-il)?|tombait(?:-il)?|tombera(?:-t-il)?|tomberait(?:-il)?)\s+un\s+(?P{names}))\b", - ] - }.get(language, []) + patterns = [ + pattern.replace("__WEEKDAY_NAMES__", names) + for pattern in self._load_cached_list("weekday.match.patterns") + ] normalized = utt.lower() for pattern in patterns: @@ -270,31 +266,18 @@ def _strip_requested_weekday_phrase(self, utt: str, weekday) -> str: _, _, phrase = weekday cleaned = re.sub(re.escape(phrase), "", utt, flags=re.IGNORECASE) - if self.lang.split("-")[0].lower() == "en": - cleaned = re.sub(r"^\s*(?:does|is|was|will)\s+", "", cleaned, - flags=re.IGNORECASE) - cleaned = re.sub(r"\b(?:fall|be)\s*$", "", cleaned, - flags=re.IGNORECASE) + for pattern in self._load_cached_list("weekday.match.strip_prefixes"): + cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE) + for pattern in self._load_cached_list("weekday.match.strip_suffixes"): + cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE) return cleaned.strip(" \t,;:.!?") def _get_leap_year_query_scope(self, utterance: str) -> str: """Return whether the user asked about the current, next, or either year.""" - language = self.lang.split("-")[0].lower() utterance = utterance.lower() - either_phrases = { - "en": ("or next", "or the next one"), - "fr": ( - "ou la prochaine", - "ou l'année prochaine", - "ou l'an prochain", - "ou la suivante", - ) - }.get(language, ()) - next_year_phrases = { - "en": ("next year",), - "fr": ("année prochaine", "an prochain") - }.get(language, ()) + either_phrases = self._load_cached_list("leap.year.scope.either_phrases") + next_year_phrases = self._load_cached_list("leap.year.scope.next_phrases") if any(phrase in utterance for phrase in either_phrases): return "either" From c447d3fac5939a49cbc67b69fb9ead8d2e4a09ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Mon, 9 Mar 2026 08:05:39 -0400 Subject: [PATCH 08/11] Scope parser caches by language --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index eae5dd26..47a9db0c 100644 --- a/__init__.py +++ b/__init__.py @@ -219,14 +219,14 @@ def _mentions_current_weekend(self, utterance: str) -> bool: def _load_cached_list(self, name: str): """Load and cache a locale list resource.""" - cache_key = f"{name}.list" + cache_key = f"{self.lang}:{name}.list" if cache_key not in self.resources.static: self.resources.static[cache_key] = self.resources.load_list_file(name) return self.resources.static[cache_key] def _load_weekday_match_map(self): """Load the locale weekday map for weekday-match parsing.""" - cache_key = "weekday.match.weekdays.value" + cache_key = f"{self.lang}:weekday.match.weekdays.value" if cache_key not in self.resources.static: weekday_map = {} for name, value in self.resources.load_named_value_file( From 54bcb52b04a89b2eabdf66474b5abe98cdd1e30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Tue, 7 Apr 2026 22:07:26 -0400 Subject: [PATCH 09/11] Refactor weekday and leap-year locale parsing --- __init__.py | 96 +++++++------------ .../en-US/intents/weekday.matches.date.intent | 48 ++-------- .../leap.year.scope.either.voc} | 0 .../leap.year.scope.next.voc} | 0 locale/en-US/weekday.entity | 7 ++ locale/en-US/weekday.match.patterns.list | 3 - .../en-US/weekday.match.strip_prefixes.list | 1 - .../en-US/weekday.match.strip_suffixes.list | 1 - locale/en-US/weekday.match.weekdays.value | 7 -- .../fr-FR/intents/weekday.matches.date.intent | 18 +--- .../leap.year.scope.either.voc} | 0 .../leap.year.scope.next.voc} | 0 locale/fr-FR/weekday.entity | 7 ++ locale/fr-FR/weekday.match.patterns.list | 1 - locale/fr-FR/weekday.match.weekdays.value | 7 -- translations/en-us/entities.json | 9 ++ translations/en-us/intents.json | 9 +- translations/fr-fr/entities.json | 9 ++ translations/fr-fr/intents.json | 18 +--- 19 files changed, 85 insertions(+), 156 deletions(-) rename locale/en-US/{leap.year.scope.either_phrases.list => vocab/leap.year.scope.either.voc} (100%) rename locale/en-US/{leap.year.scope.next_phrases.list => vocab/leap.year.scope.next.voc} (100%) create mode 100644 locale/en-US/weekday.entity delete mode 100644 locale/en-US/weekday.match.patterns.list delete mode 100644 locale/en-US/weekday.match.strip_prefixes.list delete mode 100644 locale/en-US/weekday.match.strip_suffixes.list delete mode 100644 locale/en-US/weekday.match.weekdays.value rename locale/fr-FR/{leap.year.scope.either_phrases.list => vocab/leap.year.scope.either.voc} (100%) rename locale/fr-FR/{leap.year.scope.next_phrases.list => vocab/leap.year.scope.next.voc} (100%) create mode 100644 locale/fr-FR/weekday.entity delete mode 100644 locale/fr-FR/weekday.match.patterns.list delete mode 100644 locale/fr-FR/weekday.match.weekdays.value diff --git a/__init__.py b/__init__.py index 47a9db0c..425365d7 100644 --- a/__init__.py +++ b/__init__.py @@ -217,71 +217,45 @@ def _mentions_current_weekend(self, utterance: str) -> bool: normalized_utterance = utterance.casefold() return any(phrase in normalized_utterance for phrase in current_weekend_phrases) - def _load_cached_list(self, name: str): - """Load and cache a locale list resource.""" - cache_key = f"{self.lang}:{name}.list" + def _load_cached_entity(self, name: str): + """Load and cache a locale entity resource.""" + cache_key = f"{self.lang}:{name}.entity" if cache_key not in self.resources.static: - self.resources.static[cache_key] = self.resources.load_list_file(name) + values = [] + entity_file = self.find_resource(f"{name}.entity", "vocab") + if entity_file: + with open(entity_file, encoding="utf-8") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + values.append(line) + self.resources.static[cache_key] = values return self.resources.static[cache_key] - def _load_weekday_match_map(self): - """Load the locale weekday map for weekday-match parsing.""" - cache_key = f"{self.lang}:weekday.match.weekdays.value" - if cache_key not in self.resources.static: - weekday_map = {} - for name, value in self.resources.load_named_value_file( - "weekday.match.weekdays" - ).items(): - try: - weekday_map[name.lower()] = int(value) - except (TypeError, ValueError): - continue - self.resources.static[cache_key] = weekday_map - return self.resources.static[cache_key] - - def _extract_requested_weekday(self, utt: str): - """Extract the weekday requested by yes/no weekday-check intents.""" - weekday_map = self._load_weekday_match_map() - if not utt or not weekday_map: + def _get_requested_weekday(self, message): + """Extract the requested weekday from intent-matched entities.""" + weekday_name = (message.data.get("weekday") or "").strip().lower() + if not weekday_name: return None - names = "|".join(map(re.escape, weekday_map)) - patterns = [ - pattern.replace("__WEEKDAY_NAMES__", names) - for pattern in self._load_cached_list("weekday.match.patterns") - ] - - normalized = utt.lower() - for pattern in patterns: - match = re.search(pattern, normalized) - if match: - weekday = match.group("weekday") - return weekday_map[weekday], weekday, match.group("phrase") - return None - - def _strip_requested_weekday_phrase(self, utt: str, weekday) -> str: - """Remove the weekday-check wording before date parsing.""" - if not utt or weekday is None: - return utt + weekdays = [day.lower() for day in self._load_cached_entity("weekday")] + try: + weekday_index = weekdays.index(weekday_name) + except ValueError: + return None + return weekday_index, self._render_weekday(weekday_index) - _, _, phrase = weekday - cleaned = re.sub(re.escape(phrase), "", utt, flags=re.IGNORECASE) - for pattern in self._load_cached_list("weekday.match.strip_prefixes"): - cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE) - for pattern in self._load_cached_list("weekday.match.strip_suffixes"): - cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE) - return cleaned.strip(" \t,;:.!?") + def _render_weekday(self, weekday_index: int) -> str: + """Render a localized weekday from a weekday index.""" + reference_monday = datetime.datetime(2024, 1, 1) + return nice_weekday(reference_monday + datetime.timedelta(days=weekday_index), + lang=self.lang) def _get_leap_year_query_scope(self, utterance: str) -> str: """Return whether the user asked about the current, next, or either year.""" - utterance = utterance.lower() - - either_phrases = self._load_cached_list("leap.year.scope.either_phrases") - next_year_phrases = self._load_cached_list("leap.year.scope.next_phrases") - - if any(phrase in utterance for phrase in either_phrases): + if self.voc_match(utterance, "leap.year.scope.either", lang=self.lang): return "either" - if any(phrase in utterance for phrase in next_year_phrases): + if self.voc_match(utterance, "leap.year.scope.next", lang=self.lang): return "next" return "current" @@ -543,7 +517,7 @@ def handle_query_date(self, message, response_type="simple"): now = self.get_datetime() # session aware try: dt, utt = extract_datetime(utt, anchorDate=now, lang=self.lang) or (now, utt) - except Exception as e: + except Exception: self.log.exception(f"failed to extract date from '{utt}'") dt = now @@ -651,18 +625,14 @@ def handle_weekday_match(self, message): """Handle yes/no questions about whether a date matches a weekday.""" now = self.get_datetime() # session aware utterance = message.data.get("utterance", "") - weekday = self._extract_requested_weekday(utterance) - date_text = message.data.get("date") - if not date_text: - date_text = self._strip_requested_weekday_phrase(utterance, weekday) - - dt, _ = extract_datetime(date_text or utterance, + weekday = self._get_requested_weekday(message) + dt, _ = extract_datetime(message.data.get("date") or utterance, anchorDate=now, lang=self.lang) or (None, None) if not dt or weekday is None: self.speak_dialog("extract.date.error") return - expected_weekday, expected_name, _ = weekday + expected_weekday, expected_name = weekday data = { "date": nice_date(dt, lang=self.lang, now=now), "weekday": expected_name, diff --git a/locale/en-US/intents/weekday.matches.date.intent b/locale/en-US/intents/weekday.matches.date.intent index 5ccc35f2..9770e29d 100644 --- a/locale/en-US/intents/weekday.matches.date.intent +++ b/locale/en-US/intents/weekday.matches.date.intent @@ -1,42 +1,6 @@ -does {date} fall on a Friday -does {date} fall on a Monday -does {date} fall on a Saturday -does {date} fall on a Sunday -does {date} fall on a Thursday -does {date} fall on a Tuesday -does {date} fall on a Wednesday -is {date} a Friday -is {date} a Monday -is {date} a Saturday -is {date} a Sunday -is {date} a Thursday -is {date} a Tuesday -is {date} a Wednesday -is {date} on a Friday -is {date} on a Monday -is {date} on a Saturday -is {date} on a Sunday -is {date} on a Thursday -is {date} on a Tuesday -is {date} on a Wednesday -was {date} a Friday -was {date} a Monday -was {date} a Saturday -was {date} a Sunday -was {date} a Thursday -was {date} a Tuesday -was {date} a Wednesday -was {date} on a Friday -was {date} on a Monday -was {date} on a Saturday -was {date} on a Sunday -was {date} on a Thursday -was {date} on a Tuesday -was {date} on a Wednesday -will {date} be on a Friday -will {date} be on a Monday -will {date} be on a Saturday -will {date} be on a Sunday -will {date} be on a Thursday -will {date} be on a Tuesday -will {date} be on a Wednesday +does {date} fall on a {weekday} +is {date} a {weekday} +is {date} on a {weekday} +was {date} a {weekday} +was {date} on a {weekday} +will {date} be on a {weekday} diff --git a/locale/en-US/leap.year.scope.either_phrases.list b/locale/en-US/vocab/leap.year.scope.either.voc similarity index 100% rename from locale/en-US/leap.year.scope.either_phrases.list rename to locale/en-US/vocab/leap.year.scope.either.voc diff --git a/locale/en-US/leap.year.scope.next_phrases.list b/locale/en-US/vocab/leap.year.scope.next.voc similarity index 100% rename from locale/en-US/leap.year.scope.next_phrases.list rename to locale/en-US/vocab/leap.year.scope.next.voc diff --git a/locale/en-US/weekday.entity b/locale/en-US/weekday.entity new file mode 100644 index 00000000..f6473647 --- /dev/null +++ b/locale/en-US/weekday.entity @@ -0,0 +1,7 @@ +monday +tuesday +wednesday +thursday +friday +saturday +sunday diff --git a/locale/en-US/weekday.match.patterns.list b/locale/en-US/weekday.match.patterns.list deleted file mode 100644 index 00743656..00000000 --- a/locale/en-US/weekday.match.patterns.list +++ /dev/null @@ -1,3 +0,0 @@ -\b(?:is|was)\b.*?\b(?P(?:on\s+)?(?:a|an)\s+(?P__WEEKDAY_NAMES__))\b -\bdoes\b.*?\b(?Pfall\s+(?:on\s+)?(?:a|an)\s+(?P__WEEKDAY_NAMES__))\b -\bwill\b.*?\bbe\s+(?P(?:on\s+)?(?:a|an)\s+(?P__WEEKDAY_NAMES__))\b diff --git a/locale/en-US/weekday.match.strip_prefixes.list b/locale/en-US/weekday.match.strip_prefixes.list deleted file mode 100644 index 38838919..00000000 --- a/locale/en-US/weekday.match.strip_prefixes.list +++ /dev/null @@ -1 +0,0 @@ -^\s*(?:does|is|was|will)\s+ diff --git a/locale/en-US/weekday.match.strip_suffixes.list b/locale/en-US/weekday.match.strip_suffixes.list deleted file mode 100644 index 300a8b14..00000000 --- a/locale/en-US/weekday.match.strip_suffixes.list +++ /dev/null @@ -1 +0,0 @@ -\b(?:fall|be)\s*$ diff --git a/locale/en-US/weekday.match.weekdays.value b/locale/en-US/weekday.match.weekdays.value deleted file mode 100644 index 2cd43286..00000000 --- a/locale/en-US/weekday.match.weekdays.value +++ /dev/null @@ -1,7 +0,0 @@ -monday,0 -tuesday,1 -wednesday,2 -thursday,3 -friday,4 -saturday,5 -sunday,6 diff --git a/locale/fr-FR/intents/weekday.matches.date.intent b/locale/fr-FR/intents/weekday.matches.date.intent index f2f9bda9..b22c4d90 100644 --- a/locale/fr-FR/intents/weekday.matches.date.intent +++ b/locale/fr-FR/intents/weekday.matches.date.intent @@ -1,14 +1,4 @@ -est-ce que {date} tombe un dimanche -est-ce que {date} tombe un jeudi -est-ce que {date} tombe un lundi -est-ce que {date} tombe un mardi -est-ce que {date} tombe un mercredi -est-ce que {date} tombe un samedi -est-ce que {date} tombe un vendredi -le {date} tombe-t-il un dimanche -le {date} tombe-t-il un jeudi -le {date} tombe-t-il un lundi -le {date} tombe-t-il un mardi -le {date} tombe-t-il un mercredi -le {date} tombe-t-il un samedi -le {date} tombe-t-il un vendredi +est-ce que {date} tombe un {weekday} +le {date} tombe-t-il un {weekday} +le {date} tombait-il un {weekday} +le {date} tombera-t-il un {weekday} diff --git a/locale/fr-FR/leap.year.scope.either_phrases.list b/locale/fr-FR/vocab/leap.year.scope.either.voc similarity index 100% rename from locale/fr-FR/leap.year.scope.either_phrases.list rename to locale/fr-FR/vocab/leap.year.scope.either.voc diff --git a/locale/fr-FR/leap.year.scope.next_phrases.list b/locale/fr-FR/vocab/leap.year.scope.next.voc similarity index 100% rename from locale/fr-FR/leap.year.scope.next_phrases.list rename to locale/fr-FR/vocab/leap.year.scope.next.voc diff --git a/locale/fr-FR/weekday.entity b/locale/fr-FR/weekday.entity new file mode 100644 index 00000000..e65d2fdf --- /dev/null +++ b/locale/fr-FR/weekday.entity @@ -0,0 +1,7 @@ +lundi +mardi +mercredi +jeudi +vendredi +samedi +dimanche diff --git a/locale/fr-FR/weekday.match.patterns.list b/locale/fr-FR/weekday.match.patterns.list deleted file mode 100644 index e221a09f..00000000 --- a/locale/fr-FR/weekday.match.patterns.list +++ /dev/null @@ -1 +0,0 @@ -\b(?P(?:tombe(?:-t-il)?|tombait(?:-il)?|tombera(?:-t-il)?|tomberait(?:-il)?)\s+un\s+(?P__WEEKDAY_NAMES__))\b diff --git a/locale/fr-FR/weekday.match.weekdays.value b/locale/fr-FR/weekday.match.weekdays.value deleted file mode 100644 index 8cbbe1e1..00000000 --- a/locale/fr-FR/weekday.match.weekdays.value +++ /dev/null @@ -1,7 +0,0 @@ -lundi,0 -mardi,1 -mercredi,2 -jeudi,3 -vendredi,4 -samedi,5 -dimanche,6 diff --git a/translations/en-us/entities.json b/translations/en-us/entities.json index 6dd525df..10a31da5 100644 --- a/translations/en-us/entities.json +++ b/translations/en-us/entities.json @@ -165,5 +165,14 @@ "Friday", "Saturday", "Sunday" + ], + "weekday.entity": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" ] } diff --git a/translations/en-us/intents.json b/translations/en-us/intents.json index 246fdbdf..0bd3c202 100644 --- a/translations/en-us/intents.json +++ b/translations/en-us/intents.json @@ -68,9 +68,12 @@ "[(I want to|do you) know] (which|what) (day|day of the week|weekday) {date} (was|is)" ], "/intents/weekday.matches.date.intent": [ - "(was|is) {date} [on] a (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)", - "does {date} fall on a (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)", - "will {date} be on a (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)" + "does {date} fall on a {weekday}", + "is {date} a {weekday}", + "is {date} on a {weekday}", + "was {date} a {weekday}", + "was {date} on a {weekday}", + "will {date} be on a {weekday}" ], "/intents/what.weekday.is.it.intent": [ "(which|what) (day of the week|weekday) is (it|today)", diff --git a/translations/fr-fr/entities.json b/translations/fr-fr/entities.json index d7d59516..9fb10149 100644 --- a/translations/fr-fr/entities.json +++ b/translations/fr-fr/entities.json @@ -143,5 +143,14 @@ "aujourd'hui", "demain", "hier" + ], + "weekday.entity": [ + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi", + "dimanche" ] } diff --git a/translations/fr-fr/intents.json b/translations/fr-fr/intents.json index 934a2e88..46d682c2 100644 --- a/translations/fr-fr/intents.json +++ b/translations/fr-fr/intents.json @@ -32,20 +32,10 @@ "l'année prochaine est-elle bissextile" ], "/intents/weekday.matches.date.intent": [ - "est-ce que {date} tombe un dimanche", - "est-ce que {date} tombe un jeudi", - "est-ce que {date} tombe un lundi", - "est-ce que {date} tombe un mardi", - "est-ce que {date} tombe un mercredi", - "est-ce que {date} tombe un samedi", - "est-ce que {date} tombe un vendredi", - "le {date} tombe-t-il un dimanche", - "le {date} tombe-t-il un jeudi", - "le {date} tombe-t-il un lundi", - "le {date} tombe-t-il un mardi", - "le {date} tombe-t-il un mercredi", - "le {date} tombe-t-il un samedi", - "le {date} tombe-t-il un vendredi" + "est-ce que {date} tombe un {weekday}", + "le {date} tombe-t-il un {weekday}", + "le {date} tombait-il un {weekday}", + "le {date} tombera-t-il un {weekday}" ], "/intents/what.time.is.it.intent": [ "quelle heure est-il", From bf664a3d20fc7837246eefeb9b44caf11975af57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Wed, 8 Apr 2026 00:05:33 -0400 Subject: [PATCH 10/11] Fix leap year dialog data --- __init__.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/__init__.py b/__init__.py index 425365d7..85ea1a17 100644 --- a/__init__.py +++ b/__init__.py @@ -764,20 +764,19 @@ def handle_is_leap_year(self, message): return if scope == "next": - dialog = ( - "leap.year.next.yes" - if calendar.isleap(next_year) - else "leap.year.next.no" - ) - self.speak_dialog(dialog, {"year": next_year}) + if calendar.isleap(next_year): + self.speak_dialog("leap.year.next.yes", {"year": next_year}) + else: + self.speak_dialog("leap.year.next.no", + {"year": get_next_leap_year(next_year)}) return - dialog = ( - "leap.year.current.yes" - if calendar.isleap(current_year) - else "leap.year.current.no" - ) - self.speak_dialog(dialog, {"year": current_year}) + if calendar.isleap(current_year): + self.speak_dialog("leap.year.current.yes", + {"year": current_year}) + else: + self.speak_dialog("leap.year.current.no", + {"year": get_next_leap_year(current_year)}) ###################################################################### # GUI / Faceplate From 66ca2483eb74da0c2110d8b3b958350d7f015f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Trellu?= Date: Thu, 9 Apr 2026 12:06:55 -0400 Subject: [PATCH 11/11] Use entity resource type for weekday loading --- __init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index 85ea1a17..ab83435a 100644 --- a/__init__.py +++ b/__init__.py @@ -29,6 +29,7 @@ from ovos_utils.time import now_local, get_next_leap_year from ovos_utterance_normalizer import UtteranceNormalizerPlugin from ovos_workshop.decorators import intent_handler +from ovos_workshop.resource_files import ResourceFile from ovos_workshop.skills import OVOSSkill from timezonefinder import TimezoneFinder @@ -221,10 +222,10 @@ def _load_cached_entity(self, name: str): """Load and cache a locale entity resource.""" cache_key = f"{self.lang}:{name}.entity" if cache_key not in self.resources.static: + entity_resource = ResourceFile(self.resources.types.entity, name) values = [] - entity_file = self.find_resource(f"{name}.entity", "vocab") - if entity_file: - with open(entity_file, encoding="utf-8") as f: + if entity_resource.file_path: + with open(entity_resource.file_path, encoding="utf-8") as f: for line in f: line = line.strip() if line and not line.startswith("#"):