From 312783919a4656dca7b72728ba1f78b3e3df4f52 Mon Sep 17 00:00:00 2001 From: tempus2016 Date: Wed, 7 Oct 2026 19:17:53 +0000 Subject: [PATCH] feat(notifications): localise the rest of the push text (#1037) Follow-up to #1041. Celebration messages, recap announcements (and their period labels), month and weekday names in the monthly report, season champion and auction dates, name lists ("A, B and C") and the panel's test-push sample names now follow HA's language too. Strings are loaded at startup and reloaded when HA's language changes, so text built outside the dispatcher is translated as well. A test push now fills {date}, {max_points}, {result_text} and {freezes_left}, which were previously sent as literal placeholders. --- custom_components/taskmate/coord_auctions.py | 9 +- custom_components/taskmate/coord_birthdays.py | 4 +- .../taskmate/coord_challenges.py | 7 +- custom_components/taskmate/coord_chores.py | 4 +- .../taskmate/coord_inspections.py | 7 +- .../taskmate/coord_notifications.py | 56 ++++++---- custom_components/taskmate/coord_points.py | 14 ++- custom_components/taskmate/coord_quests.py | 7 +- custom_components/taskmate/coord_recaps.py | 51 ++++----- custom_components/taskmate/coordinator.py | 10 +- custom_components/taskmate/notify_strings.py | 100 ++++++++++++++++++ .../taskmate/www/locales/da.json | 61 ++++++++++- .../taskmate/www/locales/de.json | 61 ++++++++++- .../taskmate/www/locales/en-GB.json | 59 ++++++++++- .../taskmate/www/locales/en.json | 59 ++++++++++- .../taskmate/www/locales/fr.json | 61 ++++++++++- .../taskmate/www/locales/nb.json | 61 ++++++++++- .../taskmate/www/locales/nn.json | 61 ++++++++++- .../taskmate/www/locales/pl.json | 59 ++++++++++- .../taskmate/www/locales/pt-BR.json | 61 ++++++++++- .../taskmate/www/locales/pt.json | 61 ++++++++++- tests/data/locale_english_baseline.json | 28 +++++ tests/test_notify_strings.py | 72 +++++++++++++ tests/test_recaps.py | 2 +- 24 files changed, 891 insertions(+), 84 deletions(-) diff --git a/custom_components/taskmate/coord_auctions.py b/custom_components/taskmate/coord_auctions.py index 0855c48c..546f4c2d 100644 --- a/custom_components/taskmate/coord_auctions.py +++ b/custom_components/taskmate/coord_auctions.py @@ -69,11 +69,6 @@ _OCCURRENCE_LIMIT = 6 -def _short_date(day: date) -> str: - """A short date like "Sat 4 Oct", for push messages (rendered server-side).""" - return f"{day.strftime('%a')} {day.day} {day.strftime('%b')}" - - def _as_int(value, default: int = 0) -> int: try: return int(value) @@ -303,7 +298,7 @@ async def async_start_auction( NOTIF_TYPE_AUCTION_OPENED, { "chore_name": auction.chore_name, - "date": _short_date(day), + "date": notify_strings.short_date(self.hass, day), "max_points": auction.max_points, "points_name": self.storage.get_points_name(), }, @@ -670,7 +665,7 @@ async def async_remove_orphaned_auctions(self) -> None: def _auction_date_label(self, auction: Auction) -> str: try: - return _short_date(date.fromisoformat(auction.occurrence)) + return notify_strings.short_date(self.hass, date.fromisoformat(auction.occurrence)) except ValueError: return auction.occurrence diff --git a/custom_components/taskmate/coord_birthdays.py b/custom_components/taskmate/coord_birthdays.py index 7c4b3d27..5d73203a 100644 --- a/custom_components/taskmate/coord_birthdays.py +++ b/custom_components/taskmate/coord_birthdays.py @@ -22,6 +22,8 @@ from homeassistant.util import dt as dt_util +from . import notify_strings + _LOGGER = logging.getLogger(__name__) DEFAULT_BIRTHDAY_MULTIPLIER = 2.0 @@ -154,7 +156,7 @@ async def async_check_birthdays(self, refresh: bool = True) -> list[str]: await self._celebrate( child, "birthday", - f"Happy birthday, {child.name}!", + notify_strings.render(self.hass, "celebrate_birthday", {"child_name": child.name}), tier=3, extra={"multiplier": self.birthday_multiplier(), **({"age": age} if age is not None else {})}, ) diff --git a/custom_components/taskmate/coord_challenges.py b/custom_components/taskmate/coord_challenges.py index 1563d7ca..5ac57532 100644 --- a/custom_components/taskmate/coord_challenges.py +++ b/custom_components/taskmate/coord_challenges.py @@ -13,6 +13,7 @@ from homeassistant.util import dt as dt_util +from . import notify_strings from .models import Challenge, PointsTransaction _LOGGER = logging.getLogger(__name__) @@ -173,7 +174,11 @@ async def _award_challenge(self, challenge: Challenge, child) -> None: await self._celebrate( child, "challenge_completed", - f"{child.name} completed the challenge '{challenge.name}'!", + notify_strings.render( + self.hass, + "celebrate_challenge_completed", + {"child_name": child.name, "challenge_name": challenge.name}, + ), tier=2, extra={"challenge_id": challenge.id, "bonus": bonus}, ) diff --git a/custom_components/taskmate/coord_chores.py b/custom_components/taskmate/coord_chores.py index 2357bc84..b703d5b9 100644 --- a/custom_components/taskmate/coord_chores.py +++ b/custom_components/taskmate/coord_chores.py @@ -9,7 +9,7 @@ from homeassistant.util import dt as dt_util -from . import images, photos +from . import images, notify_strings, photos from .chore_undo import child_can_undo, undo_window_seconds from .const import ( CHORE_NOTE_MAX_LENGTH, @@ -1416,7 +1416,7 @@ async def async_approve_chore( await self._celebrate( child, "all_chores_done", - f"{child.name} finished every chore today!", + notify_strings.render(self.hass, "celebrate_all_chores_done", {"child_name": child.name}), tier=1, ) else: diff --git a/custom_components/taskmate/coord_inspections.py b/custom_components/taskmate/coord_inspections.py index 40b1174c..b06dd9df 100644 --- a/custom_components/taskmate/coord_inspections.py +++ b/custom_components/taskmate/coord_inspections.py @@ -33,6 +33,7 @@ from homeassistant.helpers.event import async_track_point_in_time, async_track_time_change from homeassistant.util import dt as dt_util +from . import notify_strings from .const import ( NOTIF_TYPE_INSPECTION_PASSED, NOTIF_TYPE_INSPECTION_REMINDER, @@ -306,7 +307,11 @@ async def async_pass_inspection(self, inspection_id: str, *, bonus: int | None = await self._celebrate( child, "inspection_passed", - f"{child.name}'s {record['chore_name']} passed inspection!", + notify_strings.render( + self.hass, + "celebrate_inspection_passed", + {"child_name": child.name, "chore_name": record["chore_name"]}, + ), tier=1, extra={"inspection_id": record["id"], "bonus": record["bonus"]}, ) diff --git a/custom_components/taskmate/coord_notifications.py b/custom_components/taskmate/coord_notifications.py index 5e740ead..160c7570 100644 --- a/custom_components/taskmate/coord_notifications.py +++ b/custom_components/taskmate/coord_notifications.py @@ -15,7 +15,7 @@ import contextlib import logging from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from typing import Any from homeassistant.core import HomeAssistant, callback @@ -69,6 +69,9 @@ # original Approve/Reject ids keep working unchanged when ratings are off. _RATE_ACTION_PREFIX = "TASKMATE_RATE_" +# Stand-in names for a test push, translated like the rest (notify.sample_*). +_SAMPLE_KEYS = ("chore", "reward", "badge", "goal", "goal_reward", "wish", "pledger", "bounty", "reason", "note") + def _reject_action(hass: HomeAssistant, entry_id: str) -> dict[str, str]: """The Reject button of an approval push, with a reply box for the reason (#976).""" @@ -410,39 +413,54 @@ async def send_test(self, type_id: str) -> list[str]: meta = NOTIFICATION_TYPES_BY_ID.get(type_id) if meta is None: raise ValueError(f"Unknown notification type {type_id}") + await notify_strings.async_load(self.hass) + points_name = self.storage.get_points_name() + sample_month = date(2026, 1, 1) + sample = {key: notify_strings.text(self.hass, f"sample_{key}") for key in _SAMPLE_KEYS} ctx = { "child_name": "Alex", - "chore_name": "Tidy room", - "reward_name": "Movie night", - "badge_name": "Star Helper", + "chore_name": sample["chore"], + "reward_name": sample["reward"], + "badge_name": sample["badge"], "points": 10, "cost": 50, "streak": 7, "days": 7, "level": 5, "tier": 3, - "message": "Alex reached level 5!", - "summary": "• Alex: 5 chores, 50 Stars earned", - "month": "January 2026", - "goal_name": "Movie night fund", - "goal_reward": "a family movie night", + "message": notify_strings.render(self.hass, "celebrate_level_up", {"child_name": "Alex", "level": 5}), + "summary": notify_strings.render( + self.hass, + "weekly_digest_line", + {"child_name": "Alex", "chores": 5, "points": 50, "points_name": points_name}, + ), + "month": notify_strings.month_year(self.hass, sample_month), + "goal_name": sample["goal"], + "goal_reward": sample["goal_reward"], "multiplier": "2", "count": 3, - "wish_name": "Lego set", + "freezes_left": 2, + "wish_name": sample["wish"], "target": 600, - "pledger": "Grandma", - "bounty_name": "Wash the car", + "pledger": sample["pledger"], + "bounty_name": sample["bounty"], "minutes": 15, - "period": "January", - "item_name": "Tidy room", - "reason": "Not finished", - "reason_text": ": Not finished", + "period": notify_strings.month_name(self.hass, sample_month.month), + "item_name": sample["chore"], + "reason": sample["reason"], + "reason_text": f": {sample['reason']}", + "date": notify_strings.short_date(self.hass, sample_month), + "max_points": 20, "until": "18:20", "bonus": 10, - "note_text": ": Brilliant job!", - "points_name": self.storage.get_points_name(), + "note_text": f": {sample['note']}", + "points_name": points_name, } - await notify_strings.async_load(self.hass) + ctx["result_text"] = notify_strings.render( + self.hass, + "auction_won", + {**ctx, "points": 12}, + ) message = "[TEST] " + self._render_template(meta, ctx) cfg = self.storage.get_notification_config(type_id) nav_url = self._resolve_nav_url(cfg) diff --git a/custom_components/taskmate/coord_points.py b/custom_components/taskmate/coord_points.py index 4954e81e..40312755 100644 --- a/custom_components/taskmate/coord_points.py +++ b/custom_components/taskmate/coord_points.py @@ -8,7 +8,7 @@ from homeassistant.util import dt as dt_util -from . import photos +from . import notify_strings, photos from .models import Bonus, Child, Penalty, PointsTransaction, generate_id if TYPE_CHECKING: @@ -143,7 +143,11 @@ def _required_dates(child_id: str) -> set[str]: await self._celebrate( child, "perfect_week", - f"{child.name} earned a perfect week — +{perfect_week_bonus}!", + notify_strings.render( + self.hass, + "celebrate_perfect_week", + {"child_name": child.name, "bonus": perfect_week_bonus}, + ), tier=3, extra={"bonus": perfect_week_bonus}, ) @@ -657,7 +661,7 @@ async def _maybe_level_up(self, child) -> None: await self._celebrate( child, "level_up", - f"{child.name} reached level {lvl}!", + notify_strings.render(self.hass, "celebrate_level_up", {"child_name": child.name, "level": lvl}), tier=3 if lvl % 5 == 0 else 2, extra={"level": lvl}, ) @@ -1130,7 +1134,9 @@ async def _award_points( await self._celebrate( child, "streak_milestone", - f"{child.name} hit a {days}-day streak!", + notify_strings.render( + self.hass, "celebrate_streak_milestone", {"child_name": child.name, "days": days} + ), tier=3 if days >= 30 else 2, extra={"days": days}, ) diff --git a/custom_components/taskmate/coord_quests.py b/custom_components/taskmate/coord_quests.py index 4b5c116d..f8e45566 100644 --- a/custom_components/taskmate/coord_quests.py +++ b/custom_components/taskmate/coord_quests.py @@ -13,6 +13,7 @@ from homeassistant.util import dt as dt_util +from . import notify_strings from .models import PointsTransaction, Quest _LOGGER = logging.getLogger(__name__) @@ -223,7 +224,11 @@ async def _complete_quest(self, quest: Quest, child, prog: dict) -> None: await self._celebrate( child, "quest_completed", - f"{child.name} completed the quest '{quest.name}'!", + notify_strings.render( + self.hass, + "celebrate_quest_completed", + {"child_name": child.name, "quest_name": quest.name}, + ), tier=3, extra={"quest_id": quest.id, "bonus": bonus}, ) diff --git a/custom_components/taskmate/coord_recaps.py b/custom_components/taskmate/coord_recaps.py index ec6f4bbf..7fe5b473 100644 --- a/custom_components/taskmate/coord_recaps.py +++ b/custom_components/taskmate/coord_recaps.py @@ -28,6 +28,7 @@ from homeassistant.helpers.event import async_track_time_change from homeassistant.util import dt as dt_util +from . import notify_strings from .models import generate_id from .timewindow import parse_hhmm @@ -86,22 +87,6 @@ ) _SATURDAY_FIRST = frozenset("AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split()) -_MONTH_ABBR = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") -_MONTH_NAME = ( - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", -) - def _add_months(day: date, months: int) -> date: """The first of the month ``months`` after ``day``'s month (may be negative).""" @@ -143,15 +128,22 @@ def completed_periods(freq: str, today: date, week_start: int = 0, limit: int = day = start - timedelta(days=1) -def period_label(freq: str, start: date, end: date) -> str: - """Short English label for a period, used in notification text.""" +def period_label(freq: str, start: date, end: date, hass: Any = None) -> str: + """Short label for a period, used in notification text (HA's language).""" if freq == "weekly": - return "weekly" + return notify_strings.text(hass, "period_weekly") if freq == "monthly": - return _MONTH_NAME[start.month - 1] + return notify_strings.month_name(hass, start.month) if freq == "yearly": return str(start.year) - return f"{_MONTH_ABBR[start.month - 1]}–{_MONTH_ABBR[end.month - 1]}" + return notify_strings.render( + hass, + "period_range", + { + "start": notify_strings.text(hass, f"month_short_{start.month}"), + "end": notify_strings.text(hass, f"month_short_{end.month}"), + }, + ) def _valid_frequencies(raw: Any) -> list[str]: @@ -754,12 +746,14 @@ async def async_send_recap_notifications(self) -> int: continue first = recaps[0] label = period_label( - first["frequency"], date.fromisoformat(first["start"]), date.fromisoformat(first["end"]) + first["frequency"], date.fromisoformat(first["start"]), date.fromisoformat(first["end"]), self.hass ) if len(recaps) == 1: - message = f"✨ Your {label} recap is ready, {child.name}! Tap to watch it." + message = notify_strings.render(self.hass, "recap_ready", {"period": label, "child_name": child.name}) else: - message = f"✨ {len(recaps)} new recaps are ready, {child.name}! Tap to watch them." + message = notify_strings.render( + self.hass, "recap_ready_many", {"count": len(recaps), "child_name": child.name} + ) per_child.append( { "child_id": child_id, @@ -775,13 +769,14 @@ async def async_send_recap_notifications(self) -> int: if not per_child: return 0 - names = [c["child_name"] for c in per_child] - joined = names[0] if len(names) == 1 else ", ".join(names[:-1]) + f" and {names[-1]}" + joined = notify_strings.join_names(self.hass, [c["child_name"] for c in per_child]) labels = {c["label"] for c in per_child} if len(labels) == 1: - parent_message = f"✨ {per_child[0]['label'].capitalize()} recaps are ready for {joined}. Tap to see them." + parent_message = notify_strings.render( + self.hass, "recap_parent_one", {"period": per_child[0]["label"], "names": joined} + ) else: - parent_message = f"✨ New recaps are ready for {joined}. Tap to see them." + parent_message = notify_strings.render(self.hass, "recap_parent_many", {"names": joined}) await self.notifications.send_recap_ready( per_child, diff --git a/custom_components/taskmate/coordinator.py b/custom_components/taskmate/coordinator.py index 770e19c5..e2dbf525 100644 --- a/custom_components/taskmate/coordinator.py +++ b/custom_components/taskmate/coordinator.py @@ -136,6 +136,7 @@ def __init__(self, hass: HomeAssistant, entry_id: str) -> None: self.external_state_version: int = 0 self._unsub_surprise: Callable[[], None] | None = None self._unsub_weekly: Callable[[], None] | None = None + self._unsub_language: Callable[[], None] | None = None self._unsub_mandatory: list[Callable[[], None]] = [] # Per-build memo for the availability matrix (PERF-1). Non-None only # inside `availability_build_scope()`; see coord_chores/coord_assignments. @@ -392,6 +393,10 @@ async def async_initialize(self) -> None: """Initialize the coordinator.""" await self.storage.async_load() self.notifications.coordinator = self + # Notification wording follows HA's language (#1037): load it now, and + # again whenever the language is changed. + await notify_strings.async_load(self.hass) + self._unsub_language = notify_strings.async_watch_language(self.hass) # Wire existing parent recipients to default-on parent-audience types # that have no routes yet (e.g. a parent added after the one-time seed), # so pending-approval notifications actually reach a parent. @@ -516,7 +521,7 @@ async def _async_send_monthly_report(self) -> None: "monthly_report", { "summary": summary, - "month": month_start.strftime("%B %Y"), + "month": notify_strings.month_year(self.hass, month_start), }, ) @@ -707,7 +712,7 @@ async def _async_finalize_season(self) -> None: { "child_name": top["name"], "points": top["points"], - "month": prev_end.strftime("%B %Y"), + "month": notify_strings.month_year(self.hass, prev_end), "points_name": self.storage.get_points_name(), }, ) @@ -828,6 +833,7 @@ async def async_shutdown(self) -> None: "_unsub_tag_scanned", "_unsub_surprise", "_unsub_weekly", + "_unsub_language", "_unsub_auction_timer", ): if unsub := getattr(self, attr, None): diff --git a/custom_components/taskmate/notify_strings.py b/custom_components/taskmate/notify_strings.py index 8166ab4d..77ccbbfa 100644 --- a/custom_components/taskmate/notify_strings.py +++ b/custom_components/taskmate/notify_strings.py @@ -17,6 +17,7 @@ import json import logging from collections.abc import Mapping +from datetime import date from pathlib import Path from typing import Any @@ -73,6 +74,65 @@ "auction_called_off": "the auction for '{chore_name}' was called off. It goes back to the normal rota.", "weekly_digest_line": "• {child_name}: {chores} chores, {points} {points_name} earned", "monthly_report_line": "• {child_name}: {chores} chores, {points} {points_name}, level {level}, best streak {streak}", + # Celebration messages (the celebration push and the taskmate_celebration + # event), keyed celebrate_. + "celebrate_all_chores_done": "{child_name} finished every chore today!", + "celebrate_birthday": "Happy birthday, {child_name}!", + "celebrate_challenge_completed": "{child_name} completed the challenge '{challenge_name}'!", + "celebrate_inspection_passed": "{child_name}'s {chore_name} passed inspection!", + "celebrate_level_up": "{child_name} reached level {level}!", + "celebrate_perfect_week": "{child_name} earned a perfect week — +{bonus}!", + "celebrate_quest_completed": "{child_name} completed the quest '{quest_name}'!", + "celebrate_streak_milestone": "{child_name} hit a {days}-day streak!", + # Recap announcements: the single-recap child push is recap_ready above. + "recap_ready_many": "✨ {count} new recaps are ready, {child_name}! Tap to watch them.", + "recap_parent_one": "✨ New {period} recaps are ready for {names}. Tap to see them.", + "recap_parent_many": "✨ New recaps are ready for {names}. Tap to see them.", + "period_weekly": "weekly", + "period_range": "{start}–{end}", + # Lists, dates and month names used inside messages. + "names_and": "{names} and {last}", + "month_year": "{month} {year}", + "short_date": "{weekday} {day} {month}", + **{ + f"month_{i}": name + for i, name in enumerate( + ( + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ), + 1, + ) + }, + **{ + f"month_short_{i}": name + for i, name in enumerate( + ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"), 1 + ) + }, + # ISO weekday numbers: 1 = Monday. + **{f"weekday_short_{i}": name for i, name in enumerate(("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"), 1)}, + # Stand-in names for a test push from the panel. + "sample_chore": "Tidy room", + "sample_reward": "Movie night", + "sample_badge": "Star Helper", + "sample_goal": "Movie night fund", + "sample_goal_reward": "a family movie night", + "sample_wish": "Lego set", + "sample_pledger": "Grandma", + "sample_bounty": "Wash the car", + "sample_reason": "Not finished", + "sample_note": "Brilliant job!", # Approval push buttons and the hint for backends that can't show them. "action_approve": "Approve", "action_reject": "Reject", @@ -80,6 +140,8 @@ "approve_in_panel_hint": "Open the TaskMate panel to approve or reject.", } +_EVENT_CORE_CONFIG_UPDATE = "core_config_updated" + # language -> merged strings (DEFAULTS overlaid with the locale's own). _cache: dict[str, dict[str, str]] = {} @@ -143,3 +205,41 @@ def render(hass: Any, key: str, params: Mapping[str, Any]) -> str: except (ValueError, IndexError, KeyError, AttributeError): _LOGGER.warning("Malformed TaskMate notification text for %s, using English", key) return DEFAULTS.get(key, "").format_map(SafeDict(params)) + + +def async_watch_language(hass: Any): + """Reload the strings when HA's language changes; returns the unsubscribe.""" + + def _changed(_event) -> None: + hass.async_create_task(async_load(hass)) + + return hass.bus.async_listen(_EVENT_CORE_CONFIG_UPDATE, _changed) + + +def month_name(hass: Any, month: int) -> str: + return text(hass, f"month_{month}") + + +def month_year(hass: Any, day: date) -> str: + """Month and year, like "September 2026", in HA's language.""" + return render(hass, "month_year", {"month": month_name(hass, day.month), "year": day.year}) + + +def short_date(hass: Any, day: date) -> str: + """A short date, like "Sat 4 Oct", in HA's language.""" + return render( + hass, + "short_date", + { + "weekday": text(hass, f"weekday_short_{day.isoweekday()}"), + "day": day.day, + "month": text(hass, f"month_short_{day.month}"), + }, + ) + + +def join_names(hass: Any, names: list[str]) -> str: + """A list of names, like "Malia, Vaiha and Alex", in HA's language.""" + if len(names) <= 1: + return names[0] if names else "" + return render(hass, "names_and", {"names": ", ".join(names[:-1]), "last": names[-1]}) diff --git a/custom_components/taskmate/www/locales/da.json b/custom_components/taskmate/www/locales/da.json index 4438e770..5a6134f5 100644 --- a/custom_components/taskmate/www/locales/da.json +++ b/custom_components/taskmate/www/locales/da.json @@ -2917,7 +2917,7 @@ "notify.wish_pledged": "💝 {pledger} har lagt {points} {points_name} i dit ønske \"{wish_name}\"!", "notify.bounty_posted": "🏁 Ny dusør: {bounty_name} — {points} {points_name}. Den første, der tager den, får den!", "notify.bounty_claim_lapsing": "⏳ {child_name}, {minutes} minutter tilbage til at gøre \"{bounty_name}\" færdig, før den kommer tilbage på tavlen.", - "notify.recap_ready": "✨ Dit {period}-tilbageblik er klar, {child_name}! Tryk for at se det.", + "notify.recap_ready": "✨ Dit tilbageblik er klar ({period}), {child_name}! Tryk for at se det.", "notify.item_rejected": "↩️ {child_name}, \"{item_name}\" blev sendt tilbage{reason_text}", "notify.auction_opened": "🔨 Ny auktion: {chore_name} den {date}, op til {max_points} {points_name}. Laveste bud vinder!", "notify.auction_closing": "⏳ {child_name}, budrunden på \"{chore_name}\" lukker om {minutes} minutter.", @@ -2933,5 +2933,62 @@ "notify.action_approve": "Godkend", "notify.action_reject": "Afvis", "notify.action_reject_placeholder": "Begrundelse (valgfri)", - "notify.approve_in_panel_hint": "Åbn TaskMate-panelet for at godkende eller afvise." + "notify.approve_in_panel_hint": "Åbn TaskMate-panelet for at godkende eller afvise.", + "notify.celebrate_all_chores_done": "{child_name} har klaret alle dagens opgaver!", + "notify.celebrate_birthday": "Tillykke med fødselsdagen, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} har klaret udfordringen \"{challenge_name}\"!", + "notify.celebrate_inspection_passed": "{child_name}s \"{chore_name}\" bestod inspektionen!", + "notify.celebrate_level_up": "{child_name} er nået til niveau {level}!", + "notify.celebrate_perfect_week": "{child_name} har haft en perfekt uge — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} har gennemført missionen \"{quest_name}\"!", + "notify.celebrate_streak_milestone": "{child_name} har nået en stime på {days} dage!", + "notify.recap_ready_many": "✨ {count} nye tilbageblik er klar, {child_name}! Tryk for at se dem.", + "notify.recap_parent_one": "✨ Nye tilbageblik ({period}) er klar til {names}. Tryk for at se dem.", + "notify.recap_parent_many": "✨ Nye tilbageblik er klar til {names}. Tryk for at se dem.", + "notify.period_weekly": "uge", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} og {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday} d. {day}. {month}", + "notify.month_1": "januar", + "notify.month_2": "februar", + "notify.month_3": "marts", + "notify.month_4": "april", + "notify.month_5": "maj", + "notify.month_6": "juni", + "notify.month_7": "juli", + "notify.month_8": "august", + "notify.month_9": "september", + "notify.month_10": "oktober", + "notify.month_11": "november", + "notify.month_12": "december", + "notify.month_short_1": "jan.", + "notify.month_short_2": "feb.", + "notify.month_short_3": "mar.", + "notify.month_short_4": "apr.", + "notify.month_short_5": "maj", + "notify.month_short_6": "jun.", + "notify.month_short_7": "jul.", + "notify.month_short_8": "aug.", + "notify.month_short_9": "sep.", + "notify.month_short_10": "okt.", + "notify.month_short_11": "nov.", + "notify.month_short_12": "dec.", + "notify.weekday_short_1": "man.", + "notify.weekday_short_2": "tir.", + "notify.weekday_short_3": "ons.", + "notify.weekday_short_4": "tor.", + "notify.weekday_short_5": "fre.", + "notify.weekday_short_6": "lør.", + "notify.weekday_short_7": "søn.", + "notify.sample_chore": "Ryd op på værelset", + "notify.sample_reward": "Filmaften", + "notify.sample_badge": "Superhjælper", + "notify.sample_goal": "Filmaftenskasse", + "notify.sample_goal_reward": "en filmaften for hele familien", + "notify.sample_wish": "Lego-sæt", + "notify.sample_pledger": "Mormor", + "notify.sample_bounty": "Vask bilen", + "notify.sample_reason": "Ikke færdig", + "notify.sample_note": "Flot klaret!" } diff --git a/custom_components/taskmate/www/locales/de.json b/custom_components/taskmate/www/locales/de.json index b3bf0b68..90a7c95a 100644 --- a/custom_components/taskmate/www/locales/de.json +++ b/custom_components/taskmate/www/locales/de.json @@ -2917,7 +2917,7 @@ "notify.wish_pledged": "💝 {pledger} hat {points} {points_name} zu deinem Wunsch „{wish_name}“ beigesteuert!", "notify.bounty_posted": "🏁 Neue Prämie: {bounty_name} – {points} {points_name}. Wer sie zuerst beansprucht, bekommt sie!", "notify.bounty_claim_lapsing": "⏳ {child_name}, noch {minutes} Minuten, um „{bounty_name}“ zu erledigen, bevor sie wieder frei wird.", - "notify.recap_ready": "✨ Dein Rückblick für {period} ist fertig, {child_name}! Tippe, um ihn anzusehen.", + "notify.recap_ready": "✨ Dein Rückblick ist fertig ({period}), {child_name}! Tippe, um ihn anzusehen.", "notify.item_rejected": "↩️ {child_name}, „{item_name}“ wurde zurückgegeben{reason_text}", "notify.auction_opened": "🔨 Neue Auktion: {chore_name} am {date}, bis zu {max_points} {points_name}. Das niedrigste Gebot gewinnt!", "notify.auction_closing": "⏳ {child_name}, die Gebote für „{chore_name}“ enden in {minutes} Minuten.", @@ -2933,5 +2933,62 @@ "notify.action_approve": "Genehmigen", "notify.action_reject": "Ablehnen", "notify.action_reject_placeholder": "Grund (optional)", - "notify.approve_in_panel_hint": "Öffne das TaskMate-Panel, um zu genehmigen oder abzulehnen." + "notify.approve_in_panel_hint": "Öffne das TaskMate-Panel, um zu genehmigen oder abzulehnen.", + "notify.celebrate_all_chores_done": "{child_name} hat heute alle Aufgaben erledigt!", + "notify.celebrate_birthday": "Alles Gute zum Geburtstag, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} hat die Challenge „{challenge_name}“ geschafft!", + "notify.celebrate_inspection_passed": "„{chore_name}“ von {child_name} hat die Prüfung bestanden!", + "notify.celebrate_level_up": "{child_name} hat Level {level} erreicht!", + "notify.celebrate_perfect_week": "{child_name} hat eine perfekte Woche geschafft – +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} hat das Abenteuer „{quest_name}“ geschafft!", + "notify.celebrate_streak_milestone": "{child_name} hat eine {days}-Tage-Serie geschafft!", + "notify.recap_ready_many": "✨ {count} neue Rückblicke sind fertig, {child_name}! Tippe, um sie anzusehen.", + "notify.recap_parent_one": "✨ Neue Rückblicke ({period}) sind fertig für {names}. Tippe, um sie anzusehen.", + "notify.recap_parent_many": "✨ Neue Rückblicke sind fertig für {names}. Tippe, um sie anzusehen.", + "notify.period_weekly": "Woche", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} und {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday}, {day}. {month}", + "notify.month_1": "Januar", + "notify.month_2": "Februar", + "notify.month_3": "März", + "notify.month_4": "April", + "notify.month_5": "Mai", + "notify.month_6": "Juni", + "notify.month_7": "Juli", + "notify.month_8": "August", + "notify.month_9": "September", + "notify.month_10": "Oktober", + "notify.month_11": "November", + "notify.month_12": "Dezember", + "notify.month_short_1": "Jan", + "notify.month_short_2": "Feb", + "notify.month_short_3": "Mär", + "notify.month_short_4": "Apr", + "notify.month_short_5": "Mai", + "notify.month_short_6": "Jun", + "notify.month_short_7": "Jul", + "notify.month_short_8": "Aug", + "notify.month_short_9": "Sep", + "notify.month_short_10": "Okt", + "notify.month_short_11": "Nov", + "notify.month_short_12": "Dez", + "notify.weekday_short_1": "Mo", + "notify.weekday_short_2": "Di", + "notify.weekday_short_3": "Mi", + "notify.weekday_short_4": "Do", + "notify.weekday_short_5": "Fr", + "notify.weekday_short_6": "Sa", + "notify.weekday_short_7": "So", + "notify.sample_chore": "Zimmer aufräumen", + "notify.sample_reward": "Filmabend", + "notify.sample_badge": "Super-Helfer", + "notify.sample_goal": "Filmabend-Kasse", + "notify.sample_goal_reward": "einen Familien-Filmabend", + "notify.sample_wish": "Lego-Set", + "notify.sample_pledger": "Oma", + "notify.sample_bounty": "Auto waschen", + "notify.sample_reason": "Nicht fertig", + "notify.sample_note": "Super gemacht!" } diff --git a/custom_components/taskmate/www/locales/en-GB.json b/custom_components/taskmate/www/locales/en-GB.json index 29849ce4..15d21949 100644 --- a/custom_components/taskmate/www/locales/en-GB.json +++ b/custom_components/taskmate/www/locales/en-GB.json @@ -2933,5 +2933,62 @@ "notify.action_approve": "Approve", "notify.action_reject": "Reject", "notify.action_reject_placeholder": "Reason (optional)", - "notify.approve_in_panel_hint": "Open the TaskMate panel to approve or reject." + "notify.approve_in_panel_hint": "Open the TaskMate panel to approve or reject.", + "notify.celebrate_all_chores_done": "{child_name} finished every chore today!", + "notify.celebrate_birthday": "Happy birthday, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} completed the challenge '{challenge_name}'!", + "notify.celebrate_inspection_passed": "{child_name}'s {chore_name} passed inspection!", + "notify.celebrate_level_up": "{child_name} reached level {level}!", + "notify.celebrate_perfect_week": "{child_name} earned a perfect week — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} completed the quest '{quest_name}'!", + "notify.celebrate_streak_milestone": "{child_name} hit a {days}-day streak!", + "notify.recap_ready_many": "✨ {count} new recaps are ready, {child_name}! Tap to watch them.", + "notify.recap_parent_one": "✨ New {period} recaps are ready for {names}. Tap to see them.", + "notify.recap_parent_many": "✨ New recaps are ready for {names}. Tap to see them.", + "notify.period_weekly": "weekly", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} and {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday} {day} {month}", + "notify.month_1": "January", + "notify.month_2": "February", + "notify.month_3": "March", + "notify.month_4": "April", + "notify.month_5": "May", + "notify.month_6": "June", + "notify.month_7": "July", + "notify.month_8": "August", + "notify.month_9": "September", + "notify.month_10": "October", + "notify.month_11": "November", + "notify.month_12": "December", + "notify.month_short_1": "Jan", + "notify.month_short_2": "Feb", + "notify.month_short_3": "Mar", + "notify.month_short_4": "Apr", + "notify.month_short_5": "May", + "notify.month_short_6": "Jun", + "notify.month_short_7": "Jul", + "notify.month_short_8": "Aug", + "notify.month_short_9": "Sep", + "notify.month_short_10": "Oct", + "notify.month_short_11": "Nov", + "notify.month_short_12": "Dec", + "notify.weekday_short_1": "Mon", + "notify.weekday_short_2": "Tue", + "notify.weekday_short_3": "Wed", + "notify.weekday_short_4": "Thu", + "notify.weekday_short_5": "Fri", + "notify.weekday_short_6": "Sat", + "notify.weekday_short_7": "Sun", + "notify.sample_chore": "Tidy room", + "notify.sample_reward": "Movie night", + "notify.sample_badge": "Star Helper", + "notify.sample_goal": "Movie night fund", + "notify.sample_goal_reward": "a family movie night", + "notify.sample_wish": "Lego set", + "notify.sample_pledger": "Grandma", + "notify.sample_bounty": "Wash the car", + "notify.sample_reason": "Not finished", + "notify.sample_note": "Brilliant job!" } diff --git a/custom_components/taskmate/www/locales/en.json b/custom_components/taskmate/www/locales/en.json index 29849ce4..15d21949 100644 --- a/custom_components/taskmate/www/locales/en.json +++ b/custom_components/taskmate/www/locales/en.json @@ -2933,5 +2933,62 @@ "notify.action_approve": "Approve", "notify.action_reject": "Reject", "notify.action_reject_placeholder": "Reason (optional)", - "notify.approve_in_panel_hint": "Open the TaskMate panel to approve or reject." + "notify.approve_in_panel_hint": "Open the TaskMate panel to approve or reject.", + "notify.celebrate_all_chores_done": "{child_name} finished every chore today!", + "notify.celebrate_birthday": "Happy birthday, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} completed the challenge '{challenge_name}'!", + "notify.celebrate_inspection_passed": "{child_name}'s {chore_name} passed inspection!", + "notify.celebrate_level_up": "{child_name} reached level {level}!", + "notify.celebrate_perfect_week": "{child_name} earned a perfect week — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} completed the quest '{quest_name}'!", + "notify.celebrate_streak_milestone": "{child_name} hit a {days}-day streak!", + "notify.recap_ready_many": "✨ {count} new recaps are ready, {child_name}! Tap to watch them.", + "notify.recap_parent_one": "✨ New {period} recaps are ready for {names}. Tap to see them.", + "notify.recap_parent_many": "✨ New recaps are ready for {names}. Tap to see them.", + "notify.period_weekly": "weekly", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} and {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday} {day} {month}", + "notify.month_1": "January", + "notify.month_2": "February", + "notify.month_3": "March", + "notify.month_4": "April", + "notify.month_5": "May", + "notify.month_6": "June", + "notify.month_7": "July", + "notify.month_8": "August", + "notify.month_9": "September", + "notify.month_10": "October", + "notify.month_11": "November", + "notify.month_12": "December", + "notify.month_short_1": "Jan", + "notify.month_short_2": "Feb", + "notify.month_short_3": "Mar", + "notify.month_short_4": "Apr", + "notify.month_short_5": "May", + "notify.month_short_6": "Jun", + "notify.month_short_7": "Jul", + "notify.month_short_8": "Aug", + "notify.month_short_9": "Sep", + "notify.month_short_10": "Oct", + "notify.month_short_11": "Nov", + "notify.month_short_12": "Dec", + "notify.weekday_short_1": "Mon", + "notify.weekday_short_2": "Tue", + "notify.weekday_short_3": "Wed", + "notify.weekday_short_4": "Thu", + "notify.weekday_short_5": "Fri", + "notify.weekday_short_6": "Sat", + "notify.weekday_short_7": "Sun", + "notify.sample_chore": "Tidy room", + "notify.sample_reward": "Movie night", + "notify.sample_badge": "Star Helper", + "notify.sample_goal": "Movie night fund", + "notify.sample_goal_reward": "a family movie night", + "notify.sample_wish": "Lego set", + "notify.sample_pledger": "Grandma", + "notify.sample_bounty": "Wash the car", + "notify.sample_reason": "Not finished", + "notify.sample_note": "Brilliant job!" } diff --git a/custom_components/taskmate/www/locales/fr.json b/custom_components/taskmate/www/locales/fr.json index 7ae1c0dd..6541b289 100644 --- a/custom_components/taskmate/www/locales/fr.json +++ b/custom_components/taskmate/www/locales/fr.json @@ -2917,7 +2917,7 @@ "notify.wish_pledged": "💝 {pledger} a ajouté {points} {points_name} à ton souhait « {wish_name} » !", "notify.bounty_posted": "🏁 Nouvelle prime : {bounty_name} — {points} {points_name}. Le premier qui la réclame la remporte !", "notify.bounty_claim_lapsing": "⏳ {child_name}, plus que {minutes} minutes pour finir « {bounty_name} » avant qu'elle ne retourne au tableau.", - "notify.recap_ready": "✨ Ton récap {period} est prêt, {child_name} ! Touche pour le regarder.", + "notify.recap_ready": "✨ Ton récap est prêt ({period}), {child_name} ! Touche pour le regarder.", "notify.item_rejected": "↩️ {child_name}, « {item_name} » a été renvoyé{reason_text}", "notify.auction_opened": "🔨 Nouvelle enchère : {chore_name} le {date}, jusqu'à {max_points} {points_name}. L'offre la plus basse gagne !", "notify.auction_closing": "⏳ {child_name}, les enchères pour « {chore_name} » se terminent dans {minutes} minutes.", @@ -2933,5 +2933,62 @@ "notify.action_approve": "Valider", "notify.action_reject": "Refuser", "notify.action_reject_placeholder": "Raison (facultatif)", - "notify.approve_in_panel_hint": "Ouvre le panneau TaskMate pour valider ou refuser." + "notify.approve_in_panel_hint": "Ouvre le panneau TaskMate pour valider ou refuser.", + "notify.celebrate_all_chores_done": "{child_name} a terminé toutes ses tâches aujourd'hui !", + "notify.celebrate_birthday": "Joyeux anniversaire, {child_name} !", + "notify.celebrate_challenge_completed": "{child_name} a réussi le défi « {challenge_name} » !", + "notify.celebrate_inspection_passed": "« {chore_name} » de {child_name} a réussi l'inspection !", + "notify.celebrate_level_up": "{child_name} a atteint le niveau {level} !", + "notify.celebrate_perfect_week": "{child_name} a fait une semaine parfaite — +{bonus} !", + "notify.celebrate_quest_completed": "{child_name} a terminé la quête « {quest_name} » !", + "notify.celebrate_streak_milestone": "{child_name} a atteint une série de {days} jours !", + "notify.recap_ready_many": "✨ {count} nouveaux récaps sont prêts, {child_name} ! Touche pour les regarder.", + "notify.recap_parent_one": "✨ Nouveaux récaps prêts ({period}) pour {names}. Touche pour les voir.", + "notify.recap_parent_many": "✨ Nouveaux récaps prêts pour {names}. Touche pour les voir.", + "notify.period_weekly": "semaine", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} et {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday} {day} {month}", + "notify.month_1": "janvier", + "notify.month_2": "février", + "notify.month_3": "mars", + "notify.month_4": "avril", + "notify.month_5": "mai", + "notify.month_6": "juin", + "notify.month_7": "juillet", + "notify.month_8": "août", + "notify.month_9": "septembre", + "notify.month_10": "octobre", + "notify.month_11": "novembre", + "notify.month_12": "décembre", + "notify.month_short_1": "janv.", + "notify.month_short_2": "févr.", + "notify.month_short_3": "mars", + "notify.month_short_4": "avr.", + "notify.month_short_5": "mai", + "notify.month_short_6": "juin", + "notify.month_short_7": "juil.", + "notify.month_short_8": "août", + "notify.month_short_9": "sept.", + "notify.month_short_10": "oct.", + "notify.month_short_11": "nov.", + "notify.month_short_12": "déc.", + "notify.weekday_short_1": "lun.", + "notify.weekday_short_2": "mar.", + "notify.weekday_short_3": "mer.", + "notify.weekday_short_4": "jeu.", + "notify.weekday_short_5": "ven.", + "notify.weekday_short_6": "sam.", + "notify.weekday_short_7": "dim.", + "notify.sample_chore": "Ranger la chambre", + "notify.sample_reward": "Soirée cinéma", + "notify.sample_badge": "Super assistant", + "notify.sample_goal": "Cagnotte soirée cinéma", + "notify.sample_goal_reward": "une soirée cinéma en famille", + "notify.sample_wish": "Boîte de Lego", + "notify.sample_pledger": "Mamie", + "notify.sample_bounty": "Laver la voiture", + "notify.sample_reason": "Pas terminé", + "notify.sample_note": "Bravo, super travail !" } diff --git a/custom_components/taskmate/www/locales/nb.json b/custom_components/taskmate/www/locales/nb.json index adfcddf5..703f2330 100644 --- a/custom_components/taskmate/www/locales/nb.json +++ b/custom_components/taskmate/www/locales/nb.json @@ -2917,7 +2917,7 @@ "notify.wish_pledged": "💝 {pledger} har lagt {points} {points_name} i ønsket ditt «{wish_name}»!", "notify.bounty_posted": "🏁 Ny dusør: {bounty_name} — {points} {points_name}. Førstemann som tar den, får den!", "notify.bounty_claim_lapsing": "⏳ {child_name}, {minutes} minutter igjen til å fullføre «{bounty_name}» før den går tilbake på tavla.", - "notify.recap_ready": "✨ {period}-tilbakeblikket ditt er klart, {child_name}! Trykk for å se det.", + "notify.recap_ready": "✨ Tilbakeblikket ditt er klart ({period}), {child_name}! Trykk for å se det.", "notify.item_rejected": "↩️ {child_name}, «{item_name}» ble sendt tilbake{reason_text}", "notify.auction_opened": "🔨 Ny auksjon: {chore_name} den {date}, opptil {max_points} {points_name}. Laveste bud vinner!", "notify.auction_closing": "⏳ {child_name}, budrunden på «{chore_name}» stenger om {minutes} minutter.", @@ -2933,5 +2933,62 @@ "notify.action_approve": "Godkjenn", "notify.action_reject": "Avvis", "notify.action_reject_placeholder": "Grunn (valgfritt)", - "notify.approve_in_panel_hint": "Åpne TaskMate-panelet for å godkjenne eller avvise." + "notify.approve_in_panel_hint": "Åpne TaskMate-panelet for å godkjenne eller avvise.", + "notify.celebrate_all_chores_done": "{child_name} har gjort alle dagens oppgaver!", + "notify.celebrate_birthday": "Gratulerer med dagen, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} har fullført utfordringen «{challenge_name}»!", + "notify.celebrate_inspection_passed": "«{chore_name}» til {child_name} besto inspeksjonen!", + "notify.celebrate_level_up": "{child_name} har nådd nivå {level}!", + "notify.celebrate_perfect_week": "{child_name} har hatt en perfekt uke — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} har fullført oppdraget «{quest_name}»!", + "notify.celebrate_streak_milestone": "{child_name} har nådd en rekke på {days} dager!", + "notify.recap_ready_many": "✨ {count} nye tilbakeblikk er klare, {child_name}! Trykk for å se dem.", + "notify.recap_parent_one": "✨ Nye tilbakeblikk ({period}) er klare for {names}. Trykk for å se dem.", + "notify.recap_parent_many": "✨ Nye tilbakeblikk er klare for {names}. Trykk for å se dem.", + "notify.period_weekly": "uke", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} og {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday} {day}. {month}", + "notify.month_1": "januar", + "notify.month_2": "februar", + "notify.month_3": "mars", + "notify.month_4": "april", + "notify.month_5": "mai", + "notify.month_6": "juni", + "notify.month_7": "juli", + "notify.month_8": "august", + "notify.month_9": "september", + "notify.month_10": "oktober", + "notify.month_11": "november", + "notify.month_12": "desember", + "notify.month_short_1": "jan.", + "notify.month_short_2": "feb.", + "notify.month_short_3": "mar.", + "notify.month_short_4": "apr.", + "notify.month_short_5": "mai", + "notify.month_short_6": "jun.", + "notify.month_short_7": "jul.", + "notify.month_short_8": "aug.", + "notify.month_short_9": "sep.", + "notify.month_short_10": "okt.", + "notify.month_short_11": "nov.", + "notify.month_short_12": "des.", + "notify.weekday_short_1": "man.", + "notify.weekday_short_2": "tir.", + "notify.weekday_short_3": "ons.", + "notify.weekday_short_4": "tor.", + "notify.weekday_short_5": "fre.", + "notify.weekday_short_6": "lør.", + "notify.weekday_short_7": "søn.", + "notify.sample_chore": "Rydd rommet", + "notify.sample_reward": "Filmkveld", + "notify.sample_badge": "Superhjelper", + "notify.sample_goal": "Filmkveldskasse", + "notify.sample_goal_reward": "en filmkveld for hele familien", + "notify.sample_wish": "Lego-sett", + "notify.sample_pledger": "Bestemor", + "notify.sample_bounty": "Vask bilen", + "notify.sample_reason": "Ikke ferdig", + "notify.sample_note": "Kjempebra jobba!" } diff --git a/custom_components/taskmate/www/locales/nn.json b/custom_components/taskmate/www/locales/nn.json index 44a19d1b..7925c92a 100644 --- a/custom_components/taskmate/www/locales/nn.json +++ b/custom_components/taskmate/www/locales/nn.json @@ -2917,7 +2917,7 @@ "notify.wish_pledged": "💝 {pledger} har lagt {points} {points_name} i ønsket ditt «{wish_name}»!", "notify.bounty_posted": "🏁 Ny dusør: {bounty_name} — {points} {points_name}. Den første som tek ho, får ho!", "notify.bounty_claim_lapsing": "⏳ {child_name}, {minutes} minutt att til å fullføre «{bounty_name}» før ho går tilbake på tavla.", - "notify.recap_ready": "✨ {period}-tilbakeblikket ditt er klart, {child_name}! Trykk for å sjå det.", + "notify.recap_ready": "✨ Tilbakeblikket ditt er klart ({period}), {child_name}! Trykk for å sjå det.", "notify.item_rejected": "↩️ {child_name}, «{item_name}» vart sendt tilbake{reason_text}", "notify.auction_opened": "🔨 Ny auksjon: {chore_name} den {date}, opptil {max_points} {points_name}. Lågaste bod vinn!", "notify.auction_closing": "⏳ {child_name}, bodrunden på «{chore_name}» stengjer om {minutes} minutt.", @@ -2933,5 +2933,62 @@ "notify.action_approve": "Godkjenn", "notify.action_reject": "Avvis", "notify.action_reject_placeholder": "Grunn (valfritt)", - "notify.approve_in_panel_hint": "Opne TaskMate-panelet for å godkjenne eller avvise." + "notify.approve_in_panel_hint": "Opne TaskMate-panelet for å godkjenne eller avvise.", + "notify.celebrate_all_chores_done": "{child_name} har gjort alle oppgåvene i dag!", + "notify.celebrate_birthday": "Gratulerer med dagen, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} har fullført utfordringa «{challenge_name}»!", + "notify.celebrate_inspection_passed": "«{chore_name}» til {child_name} bestod inspeksjonen!", + "notify.celebrate_level_up": "{child_name} har nådd nivå {level}!", + "notify.celebrate_perfect_week": "{child_name} har hatt ei perfekt veke — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} har fullført oppdraget «{quest_name}»!", + "notify.celebrate_streak_milestone": "{child_name} har nådd ei rekkje på {days} dagar!", + "notify.recap_ready_many": "✨ {count} nye tilbakeblikk er klare, {child_name}! Trykk for å sjå dei.", + "notify.recap_parent_one": "✨ Nye tilbakeblikk ({period}) er klare for {names}. Trykk for å sjå dei.", + "notify.recap_parent_many": "✨ Nye tilbakeblikk er klare for {names}. Trykk for å sjå dei.", + "notify.period_weekly": "veke", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} og {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday} {day}. {month}", + "notify.month_1": "januar", + "notify.month_2": "februar", + "notify.month_3": "mars", + "notify.month_4": "april", + "notify.month_5": "mai", + "notify.month_6": "juni", + "notify.month_7": "juli", + "notify.month_8": "august", + "notify.month_9": "september", + "notify.month_10": "oktober", + "notify.month_11": "november", + "notify.month_12": "desember", + "notify.month_short_1": "jan.", + "notify.month_short_2": "feb.", + "notify.month_short_3": "mar.", + "notify.month_short_4": "apr.", + "notify.month_short_5": "mai", + "notify.month_short_6": "jun.", + "notify.month_short_7": "jul.", + "notify.month_short_8": "aug.", + "notify.month_short_9": "sep.", + "notify.month_short_10": "okt.", + "notify.month_short_11": "nov.", + "notify.month_short_12": "des.", + "notify.weekday_short_1": "mån.", + "notify.weekday_short_2": "tys.", + "notify.weekday_short_3": "ons.", + "notify.weekday_short_4": "tor.", + "notify.weekday_short_5": "fre.", + "notify.weekday_short_6": "lau.", + "notify.weekday_short_7": "sun.", + "notify.sample_chore": "Rydd rommet", + "notify.sample_reward": "Filmkveld", + "notify.sample_badge": "Superhjelpar", + "notify.sample_goal": "Filmkveldskasse", + "notify.sample_goal_reward": "ein filmkveld for heile familien", + "notify.sample_wish": "Lego-sett", + "notify.sample_pledger": "Bestemor", + "notify.sample_bounty": "Vask bilen", + "notify.sample_reason": "Ikkje ferdig", + "notify.sample_note": "Kjempebra jobba!" } diff --git a/custom_components/taskmate/www/locales/pl.json b/custom_components/taskmate/www/locales/pl.json index f6264917..d7e8bf79 100644 --- a/custom_components/taskmate/www/locales/pl.json +++ b/custom_components/taskmate/www/locales/pl.json @@ -2933,5 +2933,62 @@ "notify.action_approve": "Zatwierdź", "notify.action_reject": "Odrzuć", "notify.action_reject_placeholder": "Powód (opcjonalnie)", - "notify.approve_in_panel_hint": "Otwórz panel TaskMate, aby zatwierdzić lub odrzucić." + "notify.approve_in_panel_hint": "Otwórz panel TaskMate, aby zatwierdzić lub odrzucić.", + "notify.celebrate_all_chores_done": "{child_name} wykonał(a) dziś wszystkie zadania!", + "notify.celebrate_birthday": "Wszystkiego najlepszego, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} ukończył(a) wyzwanie „{challenge_name}”!", + "notify.celebrate_inspection_passed": "„{chore_name}” ({child_name}) przeszło kontrolę!", + "notify.celebrate_level_up": "{child_name} osiągnął(-ęła) poziom {level}!", + "notify.celebrate_perfect_week": "{child_name} ma za sobą idealny tydzień — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} ukończył(a) misję „{quest_name}”!", + "notify.celebrate_streak_milestone": "{child_name} osiągnął(-ęła) serię {days} dni!", + "notify.recap_ready_many": "✨ Nowe podsumowania są gotowe ({count}), {child_name}! Dotknij, aby je obejrzeć.", + "notify.recap_parent_one": "✨ Nowe podsumowania ({period}) są gotowe dla: {names}. Dotknij, aby je zobaczyć.", + "notify.recap_parent_many": "✨ Nowe podsumowania są gotowe dla: {names}. Dotknij, aby je zobaczyć.", + "notify.period_weekly": "tydzień", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} i {last}", + "notify.month_year": "{month} {year}", + "notify.short_date": "{weekday} {day} {month}", + "notify.month_1": "styczeń", + "notify.month_2": "luty", + "notify.month_3": "marzec", + "notify.month_4": "kwiecień", + "notify.month_5": "maj", + "notify.month_6": "czerwiec", + "notify.month_7": "lipiec", + "notify.month_8": "sierpień", + "notify.month_9": "wrzesień", + "notify.month_10": "październik", + "notify.month_11": "listopad", + "notify.month_12": "grudzień", + "notify.month_short_1": "sty", + "notify.month_short_2": "lut", + "notify.month_short_3": "mar", + "notify.month_short_4": "kwi", + "notify.month_short_5": "maj", + "notify.month_short_6": "cze", + "notify.month_short_7": "lip", + "notify.month_short_8": "sie", + "notify.month_short_9": "wrz", + "notify.month_short_10": "paź", + "notify.month_short_11": "lis", + "notify.month_short_12": "gru", + "notify.weekday_short_1": "pon.", + "notify.weekday_short_2": "wt.", + "notify.weekday_short_3": "śr.", + "notify.weekday_short_4": "czw.", + "notify.weekday_short_5": "pt.", + "notify.weekday_short_6": "sob.", + "notify.weekday_short_7": "niedz.", + "notify.sample_chore": "Posprzątaj pokój", + "notify.sample_reward": "Wieczór filmowy", + "notify.sample_badge": "Super pomocnik", + "notify.sample_goal": "Skarbonka na wieczór filmowy", + "notify.sample_goal_reward": "rodzinny wieczór filmowy", + "notify.sample_wish": "Zestaw Lego", + "notify.sample_pledger": "Babcia", + "notify.sample_bounty": "Umyj samochód", + "notify.sample_reason": "Niedokończone", + "notify.sample_note": "Świetna robota!" } diff --git a/custom_components/taskmate/www/locales/pt-BR.json b/custom_components/taskmate/www/locales/pt-BR.json index 053acc45..d0c42e28 100644 --- a/custom_components/taskmate/www/locales/pt-BR.json +++ b/custom_components/taskmate/www/locales/pt-BR.json @@ -2917,7 +2917,7 @@ "notify.wish_pledged": "💝 {pledger} colocou {points} {points_name} no seu desejo \"{wish_name}\"!", "notify.bounty_posted": "🏁 Nova recompensa extra: {bounty_name} — {points} {points_name}. Quem pegar primeiro leva!", "notify.bounty_claim_lapsing": "⏳ {child_name}, faltam {minutes} minutos para terminar \"{bounty_name}\" antes que volte para o quadro.", - "notify.recap_ready": "✨ Sua retrospectiva {period} está pronta, {child_name}! Toque para assistir.", + "notify.recap_ready": "✨ Sua retrospectiva está pronta ({period}), {child_name}! Toque para assistir.", "notify.item_rejected": "↩️ {child_name}, \"{item_name}\" foi devolvido{reason_text}", "notify.auction_opened": "🔨 Novo leilão: {chore_name} em {date}, até {max_points} {points_name}. O menor lance vence!", "notify.auction_closing": "⏳ {child_name}, os lances para \"{chore_name}\" terminam em {minutes} minutos.", @@ -2933,5 +2933,62 @@ "notify.action_approve": "Aprovar", "notify.action_reject": "Rejeitar", "notify.action_reject_placeholder": "Motivo (opcional)", - "notify.approve_in_panel_hint": "Abra o painel do TaskMate para aprovar ou rejeitar." + "notify.approve_in_panel_hint": "Abra o painel do TaskMate para aprovar ou rejeitar.", + "notify.celebrate_all_chores_done": "{child_name} terminou todas as tarefas de hoje!", + "notify.celebrate_birthday": "Feliz aniversário, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} completou o desafio \"{challenge_name}\"!", + "notify.celebrate_inspection_passed": "\"{chore_name}\" de {child_name} passou na inspeção!", + "notify.celebrate_level_up": "{child_name} chegou ao nível {level}!", + "notify.celebrate_perfect_week": "{child_name} fez uma semana perfeita — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} completou a missão \"{quest_name}\"!", + "notify.celebrate_streak_milestone": "{child_name} chegou a uma sequência de {days} dias!", + "notify.recap_ready_many": "✨ {count} novas retrospectivas estão prontas, {child_name}! Toque para assistir.", + "notify.recap_parent_one": "✨ Novas retrospectivas ({period}) estão prontas para {names}. Toque para ver.", + "notify.recap_parent_many": "✨ Novas retrospectivas estão prontas para {names}. Toque para ver.", + "notify.period_weekly": "semana", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} e {last}", + "notify.month_year": "{month} de {year}", + "notify.short_date": "{weekday}, {day} {month}", + "notify.month_1": "janeiro", + "notify.month_2": "fevereiro", + "notify.month_3": "março", + "notify.month_4": "abril", + "notify.month_5": "maio", + "notify.month_6": "junho", + "notify.month_7": "julho", + "notify.month_8": "agosto", + "notify.month_9": "setembro", + "notify.month_10": "outubro", + "notify.month_11": "novembro", + "notify.month_12": "dezembro", + "notify.month_short_1": "jan", + "notify.month_short_2": "fev", + "notify.month_short_3": "mar", + "notify.month_short_4": "abr", + "notify.month_short_5": "mai", + "notify.month_short_6": "jun", + "notify.month_short_7": "jul", + "notify.month_short_8": "ago", + "notify.month_short_9": "set", + "notify.month_short_10": "out", + "notify.month_short_11": "nov", + "notify.month_short_12": "dez", + "notify.weekday_short_1": "seg", + "notify.weekday_short_2": "ter", + "notify.weekday_short_3": "qua", + "notify.weekday_short_4": "qui", + "notify.weekday_short_5": "sex", + "notify.weekday_short_6": "sáb", + "notify.weekday_short_7": "dom", + "notify.sample_chore": "Arrumar o quarto", + "notify.sample_reward": "Noite de cinema", + "notify.sample_badge": "Super ajudante", + "notify.sample_goal": "Cofrinho da noite de cinema", + "notify.sample_goal_reward": "uma noite de cinema em família", + "notify.sample_wish": "Kit de Lego", + "notify.sample_pledger": "Vovó", + "notify.sample_bounty": "Lavar o carro", + "notify.sample_reason": "Não terminou", + "notify.sample_note": "Mandou muito bem!" } diff --git a/custom_components/taskmate/www/locales/pt.json b/custom_components/taskmate/www/locales/pt.json index 41cba9f5..6eaf4538 100644 --- a/custom_components/taskmate/www/locales/pt.json +++ b/custom_components/taskmate/www/locales/pt.json @@ -2917,7 +2917,7 @@ "notify.wish_pledged": "💝 {pledger} juntou {points} {points_name} ao teu desejo \"{wish_name}\"!", "notify.bounty_posted": "🏁 Novo prémio: {bounty_name} — {points} {points_name}. Quem o pedir primeiro fica com ele!", "notify.bounty_claim_lapsing": "⏳ {child_name}, faltam {minutes} minutos para terminares \"{bounty_name}\" antes de voltar ao quadro.", - "notify.recap_ready": "✨ O teu resumo {period} está pronto, {child_name}! Toca para o ver.", + "notify.recap_ready": "✨ O teu resumo está pronto ({period}), {child_name}! Toca para o ver.", "notify.item_rejected": "↩️ {child_name}, \"{item_name}\" foi devolvido{reason_text}", "notify.auction_opened": "🔨 Novo leilão: {chore_name} em {date}, até {max_points} {points_name}. Ganha a oferta mais baixa!", "notify.auction_closing": "⏳ {child_name}, as licitações para \"{chore_name}\" terminam dentro de {minutes} minutos.", @@ -2933,5 +2933,62 @@ "notify.action_approve": "Aprovar", "notify.action_reject": "Rejeitar", "notify.action_reject_placeholder": "Motivo (opcional)", - "notify.approve_in_panel_hint": "Abre o painel do TaskMate para aprovar ou rejeitar." + "notify.approve_in_panel_hint": "Abre o painel do TaskMate para aprovar ou rejeitar.", + "notify.celebrate_all_chores_done": "{child_name} terminou todas as tarefas de hoje!", + "notify.celebrate_birthday": "Feliz aniversário, {child_name}!", + "notify.celebrate_challenge_completed": "{child_name} completou o desafio \"{challenge_name}\"!", + "notify.celebrate_inspection_passed": "\"{chore_name}\" de {child_name} passou na inspeção!", + "notify.celebrate_level_up": "{child_name} chegou ao nível {level}!", + "notify.celebrate_perfect_week": "{child_name} fez uma semana perfeita — +{bonus}!", + "notify.celebrate_quest_completed": "{child_name} completou a missão \"{quest_name}\"!", + "notify.celebrate_streak_milestone": "{child_name} chegou a uma sequência de {days} dias!", + "notify.recap_ready_many": "✨ {count} novos resumos estão prontos, {child_name}! Toca para os ver.", + "notify.recap_parent_one": "✨ Novos resumos ({period}) estão prontos para {names}. Toca para os ver.", + "notify.recap_parent_many": "✨ Novos resumos estão prontos para {names}. Toca para os ver.", + "notify.period_weekly": "semana", + "notify.period_range": "{start}–{end}", + "notify.names_and": "{names} e {last}", + "notify.month_year": "{month} de {year}", + "notify.short_date": "{weekday}, {day} {month}", + "notify.month_1": "janeiro", + "notify.month_2": "fevereiro", + "notify.month_3": "março", + "notify.month_4": "abril", + "notify.month_5": "maio", + "notify.month_6": "junho", + "notify.month_7": "julho", + "notify.month_8": "agosto", + "notify.month_9": "setembro", + "notify.month_10": "outubro", + "notify.month_11": "novembro", + "notify.month_12": "dezembro", + "notify.month_short_1": "jan", + "notify.month_short_2": "fev", + "notify.month_short_3": "mar", + "notify.month_short_4": "abr", + "notify.month_short_5": "mai", + "notify.month_short_6": "jun", + "notify.month_short_7": "jul", + "notify.month_short_8": "ago", + "notify.month_short_9": "set", + "notify.month_short_10": "out", + "notify.month_short_11": "nov", + "notify.month_short_12": "dez", + "notify.weekday_short_1": "seg", + "notify.weekday_short_2": "ter", + "notify.weekday_short_3": "qua", + "notify.weekday_short_4": "qui", + "notify.weekday_short_5": "sex", + "notify.weekday_short_6": "sáb", + "notify.weekday_short_7": "dom", + "notify.sample_chore": "Arrumar o quarto", + "notify.sample_reward": "Noite de cinema", + "notify.sample_badge": "Super ajudante", + "notify.sample_goal": "Mealheiro da noite de cinema", + "notify.sample_goal_reward": "uma noite de cinema em família", + "notify.sample_wish": "Conjunto Lego", + "notify.sample_pledger": "Avó", + "notify.sample_bounty": "Lavar o carro", + "notify.sample_reason": "Não terminado", + "notify.sample_note": "Excelente trabalho!" } diff --git a/tests/data/locale_english_baseline.json b/tests/data/locale_english_baseline.json index 9d741896..99d4b831 100644 --- a/tests/data/locale_english_baseline.json +++ b/tests/data/locale_english_baseline.json @@ -17,6 +17,8 @@ "common.design.playroom", "dashboard.tab_rotation", "notify.celebration", + "notify.month_year", + "notify.period_range", "overview.default_title", "panel.badge_tab_title", "panel.badge_table_badge", @@ -64,6 +66,20 @@ "common.design.playroom", "dashboard.tab_rotation", "notify.celebration", + "notify.month_11", + "notify.month_4", + "notify.month_8", + "notify.month_9", + "notify.month_short_1", + "notify.month_short_11", + "notify.month_short_2", + "notify.month_short_4", + "notify.month_short_6", + "notify.month_short_7", + "notify.month_short_8", + "notify.month_short_9", + "notify.month_year", + "notify.period_range", "overview.default_title", "panel.badge_name_label", "panel.badge_table_bonus", @@ -129,6 +145,9 @@ "graph.total", "inspection.tag_open", "notify.celebration", + "notify.month_year", + "notify.period_range", + "notify.short_date", "overview.default_title", "overview.footer_total_points", "panel.activity_points", @@ -205,6 +224,8 @@ "common.design.console", "common.design.playroom", "notify.celebration", + "notify.month_year", + "notify.period_range", "overview.default_title", "panel.badge_table_bonus", "panel.bounty_col_status", @@ -252,6 +273,8 @@ "common.design.console", "common.design.playroom", "notify.celebration", + "notify.month_year", + "notify.period_range", "overview.default_title", "panel.badge_table_bonus", "panel.bounty_col_status", @@ -293,6 +316,9 @@ "child.timed_cap_label", "common.design.cleanpro", "notify.celebration", + "notify.month_year", + "notify.period_range", + "notify.short_date", "overview.default_title", "panel.bounty_col_status", "panel.bounty_completed_meta", @@ -325,6 +351,7 @@ "common.total", "graph.total", "notify.celebration", + "notify.period_range", "overview.default_title", "panel.bounty_col_status", "panel.bounty_completed_meta", @@ -361,6 +388,7 @@ "common.total", "graph.total", "notify.celebration", + "notify.period_range", "overview.default_title", "panel.bounty_completed_meta", "panel.child_avatar_label", diff --git a/tests/test_notify_strings.py b/tests/test_notify_strings.py index 3237f837..3c4b27f1 100644 --- a/tests/test_notify_strings.py +++ b/tests/test_notify_strings.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import json import string from pathlib import Path @@ -33,6 +34,14 @@ def _fresh_cache(): notify_strings._cache.clear() +def asyncio_run(coro): + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + def _lang(hass, language: str): """Give the fake hass a language and a (synchronous) executor.""" hass.config = MagicMock(language=language) @@ -151,3 +160,66 @@ async def test_custom_message_template_is_left_alone(coord, hass): {"entry_id": "c1", "child_name": "Alex", "message_template": "Hey {child_name}"}, ) assert _sent(hass)["message"].startswith("Hey Alex") + + +def test_german_dates_months_and_name_lists(hass): + from datetime import date + + _lang(hass, "de") + asyncio_run(notify_strings.async_load(hass)) + assert notify_strings.short_date(hass, date(2026, 10, 4)) == "So, 4. Okt" + assert notify_strings.month_year(hass, date(2026, 3, 1)) == "März 2026" + assert notify_strings.join_names(hass, ["Malia", "Vaiha", "Alex"]) == "Malia, Vaiha und Alex" + assert notify_strings.join_names(hass, ["Malia"]) == "Malia" + + +def test_english_dates_and_name_lists_without_a_language(): + from datetime import date + + assert notify_strings.short_date(None, date(2026, 10, 4)) == "Sun 4 Oct" + assert notify_strings.month_year(None, date(2026, 9, 1)) == "September 2026" + assert notify_strings.join_names(None, ["Malia", "Vaiha"]) == "Malia and Vaiha" + + +def test_german_recap_labels(hass): + from datetime import date + + from custom_components.taskmate.coord_recaps import period_label + + _lang(hass, "de") + asyncio_run(notify_strings.async_load(hass)) + assert period_label("weekly", date(2026, 9, 7), date(2026, 9, 13), hass) == "Woche" + assert period_label("monthly", date(2026, 3, 1), date(2026, 3, 31), hass) == "März" + assert period_label("every_3_months", date(2026, 7, 1), date(2026, 9, 30), hass) == "Jul–Sep" + + +def test_language_change_reloads_the_strings(hass): + _lang(hass, "en") + listeners = {} + hass.bus.async_listen = lambda event, cb: listeners.setdefault(event, cb) + tasks = [] + hass.async_create_task = tasks.append + notify_strings.async_watch_language(hass) + hass.config.language = "fr" + listeners["core_config_updated"](None) + asyncio_run(tasks[0]) + assert notify_strings.text(hass, "action_approve") == "Valider" + + +@pytest.mark.parametrize("path", sorted(LOCALES.glob("*.json")), ids=lambda p: p.stem) +@pytest.mark.asyncio +async def test_every_test_push_fills_its_placeholders(coord, hass, path): + """A test push of each type, in each language, leaves no {placeholder} behind.""" + from custom_components.taskmate.coord_notifications import NOTIFICATION_TYPES + + _lang(hass, path.stem) + parent = ParentRecipient(name="Sam", notify_service="notify.mobile_app_phone") + coord.storage.upsert_parent_recipient(parent) + for meta in NOTIFICATION_TYPES: + coord.storage.set_notification_route(meta.id, parent.id, NotificationRoute(enabled=True)) + hass.services.async_call.reset_mock() + await coord.send_test(meta.id) + message = _sent(hass)["message"] + assert "{" not in message, f"{path.name} {meta.id}: {message}" + if path.stem not in ("en", "en-GB"): + assert "Tidy room" not in message and "Movie night" not in message, f"{path.name} {meta.id}: {message}" diff --git a/tests/test_recaps.py b/tests/test_recaps.py index 0706b61c..6edfc01e 100644 --- a/tests/test_recaps.py +++ b/tests/test_recaps.py @@ -382,7 +382,7 @@ async def test_one_push_per_child_one_grouped_parent_message(hass, clock): assert by_service["mobile_app_malia"]["data"]["tag"] == f"taskmate_recap_{coord.kids['Malia']}" assert ( by_service["mobile_app_john"]["message"] - == "✨ September recaps are ready for Malia and Vaiha. Tap to see them." + == "✨ New September recaps are ready for Malia and Vaiha. Tap to see them." ) # No stats and no image in the push. assert "image" not in by_service["mobile_app_malia"]["data"]