chore: release v1.5.0 (#67)

* chore: release v1.4.0

* feat: timezone-aware locale formatting

- Add TIMEZONE_TO_REGION map for 50+ timezones worldwide
- Combine app language with timezone region (e.g., en + Europe/Berlin → en-DE)
- Fix times displaying in wrong timezone (treated as UTC instead of local)
- Add parseLocalDateTime() to handle ISO strings without UTC conversion
- Users now get regional formatting (24h time, local date format) regardless of app language
- Swedish user with en-SE locale now gets yyyy-mm-dd format and 24h time
- German user with en-DE locale gets dd.mm.yyyy format and 24h time
- Add missing i18n translation key 'lastSent'
- Update all getSystemLocale() calls to pass app language parameter

* chore: release v1.5.0

* fix: timezone-independent test for CI (use 14:00 instead of 22:00)

* fix: make timezone test independent of server timezone
This commit is contained in:
Daniel Volz
2026-01-23 21:42:57 +01:00
committed by GitHub
parent 0a4f8c5948
commit 8e2fd0a761
26 changed files with 1830 additions and 108 deletions
+17 -17
View File
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { useAuth } from "../components/Auth";
import { useAppContext } from "../context";
import { MedicationAvatar, ConfirmModal } from "../components";
import { formatNumber, getExpiryClass } from "../utils/formatters";
import { formatNumber, getExpiryClass, getSystemLocale } from "../utils/formatters";
import type { Coverage } from "../types";
// Helper for user-specific localStorage keys
@@ -32,9 +32,9 @@ function formatFullBlisters(count: number, t: (key: string) => string): string {
}
// Helper to format open blister and loose pills
function formatOpenBlisterAndLoose(openBlisterPills: number, loosePills: number, _pillsPerBlister: number, t: (key: string) => string): string {
function formatOpenBlisterAndLoose(openBlisterPills: number, loosePills: number, pillsPerBlister: number, t: (key: string) => string): string {
if (openBlisterPills === 0 && loosePills === 0) return "-";
return `${openBlisterPills} ${t('common.pills')}`;
return `${openBlisterPills} ${t('common.of')} ${pillsPerBlister} ${t('common.pills')}`;
}
// Get total pills for a medication
@@ -184,7 +184,7 @@ export function DashboardPage() {
<span className="email-status-icon">{settings.emailEnabled && settings.shoutrrrEnabled ? "🔔" : settings.emailEnabled ? "📧" : "🔔"}</span>
<span className="email-status-text">
<span className="email-status-line">{t('dashboard.reminders.active')}</span>
{getReminderStatusContent(settings.reminderDaysBefore, settings.lowStockDays, coverage.low, coverage.all, settings.lastAutoEmailSent, settings.lastNotificationType, settings.lastNotificationChannel, t, i18n.language)}
{getReminderStatusContent(settings.reminderDaysBefore, settings.lowStockDays, coverage.low, coverage.all, settings.lastAutoEmailSent, settings.lastNotificationType, settings.lastNotificationChannel, t, getSystemLocale(i18n.language))}
</span>
{settings.emailEnabled && settings.notificationEmail && <span className="email-status-recipient"> {settings.notificationEmail}</span>}
</section>
@@ -259,7 +259,7 @@ export function DashboardPage() {
<span data-label={t('table.days')} className={textClass}>{formatNumber(row.daysLeft)}</span>
<span data-label={t('table.status')} className={`status-chip ${status.className}`}>{t(status.label)}</span>
<span data-label={t('table.runsOut')}>{row.depletionDate ?? "-"}</span>
<span data-label={t('table.autoRemind')} className="next-reminder-date">{getNextReminderForMed(row, settings.reminderDaysBefore, i18n.language)}</span>
<span data-label={t('table.autoRemind')} className="next-reminder-date">{getNextReminderForMed(row, settings.reminderDaysBefore, getSystemLocale(i18n.language))}</span>
</div>
);
})}
@@ -322,18 +322,18 @@ export function DashboardPage() {
<span className="med-icons">
{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip={t('tooltips.intakeReminders')}>🔔</span>}
{med?.notes && <span className="notes-icon info-tooltip" data-tooltip={t('tooltips.hasNotes')}>📝</span>}
</span>
)}
</span>
<span data-label={t('table.fullBlisters')} className={textClass}>{formatFullBlisters(stock.fullBlisters, t)}</span>
<span data-label={t('table.openBlister')} className={textClass}>{formatOpenBlisterAndLoose(stock.openBlisterPills, stock.loosePills, med?.pillsPerBlister ?? 1, t)}</span>
<span data-label={t('table.daysLeft')} className={textClass}>{formatNumber(row.daysLeft)}</span>
<span data-label={t('table.runsOut')}>{row.depletionDate ?? "-"}</span>
<span data-label={t('table.expiry')} className={expiryClass}>{med?.expiryDate ? new Date(med.expiryDate).toLocaleDateString(i18n.language, { day: "2-digit", month: "short", year: "2-digit" }) : "-"}</span>
<span data-label={t('table.status')} className={`status-chip ${status.className}`}>{t(status.label)}</span>
</div>
);
})}
</span>
)}
</span>
<span data-label={t('table.fullBlisters')} className={textClass}>{formatFullBlisters(stock.fullBlisters, t)}</span>
<span data-label={t('table.openBlister')} className={textClass}>{formatOpenBlisterAndLoose(stock.openBlisterPills, stock.loosePills, med?.pillsPerBlister ?? 1, t)}</span>
<span data-label={t('table.daysLeft')} className={textClass}>{formatNumber(row.daysLeft)}</span>
<span data-label={t('table.runsOut')}>{row.depletionDate ?? "-"}</span>
<span data-label={t('table.expiry')} className={expiryClass}>{med?.expiryDate ? new Date(med.expiryDate).toLocaleDateString(getSystemLocale(i18n.language), { day: "2-digit", month: "short", year: "2-digit" }) : "-"}</span>
<span data-label={t('table.status')} className={`status-chip ${status.className}`}>{t(status.label)}</span>
</div>
);
})}
</div>
</article>
</section>
+8 -7
View File
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { useAppContext } from "../context";
import { MedicationAvatar, MobileEditModal } from "../components";
import { useMedicationForm } from "../hooks";
import { formatNumber, formatDateTime } from "../utils/formatters";
import { formatNumber, formatDateTime, combineDateAndTime } from "../utils/formatters";
import { getPackageSize, FIELD_LIMITS } from "../types";
import type { Medication } from "../types";
@@ -32,6 +32,7 @@ export function MedicationsPage() {
const {
form,
setForm,
setOriginalForm,
editingId,
setEditingId,
formSaved,
@@ -153,6 +154,9 @@ export function MedicationsPage() {
// Reset form after successful save
if (!editingId) {
resetForm();
} else {
// Update originalForm so formChanged becomes false
setOriginalForm(form);
}
} catch (err) {
console.error("Save error:", err);
@@ -234,7 +238,7 @@ export function MedicationsPage() {
<div className="blister-list">
{med.blisters.map((s, idx) => (
<div key={`${med.id}-${idx}`} className="blister-row-simple">
{s.usage} {s.usage === 1 ? t('common.pill') : t('common.pills')} · {t('form.blisters.every')} {s.every} {s.every === 1 ? t('common.day') : t('common.days')} · {t('form.blisters.from')} {formatDateTime(s.start, i18n.language)}
{s.usage} {s.usage === 1 ? t('common.pill') : t('common.pills')} · {t('form.blisters.every')} {s.every} {s.every === 1 ? t('common.day') : t('common.days')} · {t('form.blisters.from')} {formatDateTime(s.start)}
</div>
))}
</div>
@@ -480,7 +484,7 @@ export function MedicationsPage() {
</button>
)}
<button type="submit" disabled={saving || hasValidationErrors || (!formChanged && (formSaved || !!editingId))}>
{saving ? t('common.saving') : formSaved && !formChanged ? t('common.saved') : t('common.save')}
{formSaved && !formChanged ? t('common.saved') : t('common.save')}
</button>
</div>
</form>
@@ -524,7 +528,4 @@ export function MedicationsPage() {
);
}
// Helper function to combine date and time into ISO datetime with Z suffix
function combineDateAndTime(date: string, time: string): string {
return `${date}T${time}:00.000Z`;
}
+3 -2
View File
@@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next";
import { useAppContext } from "../context";
import { ConfirmModal, ExportModal } from "../components";
import { getSystemLocale } from "../utils/formatters";
export function SettingsPage() {
const { t, i18n } = useTranslation();
@@ -321,13 +322,13 @@ export function SettingsPage() {
{settings.nextScheduledCheck && (
<div className="schedule-row">
<span className="schedule-label">{t('settings.schedule.nextCheck')}</span>
<span className="schedule-value">{new Date(settings.nextScheduledCheck).toLocaleString(i18n.language, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
<span className="schedule-value">{new Date(settings.nextScheduledCheck).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
</div>
)}
{settings.lastAutoEmailSent && (
<div className="schedule-row">
<span className="schedule-label">{t('settings.schedule.lastSent')}</span>
<span className="schedule-value">{new Date(settings.lastAutoEmailSent).toLocaleString(i18n.language, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
<span className="schedule-value">{new Date(settings.lastAutoEmailSent).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
</div>
)}
</div>