From 69f891210e1597af60a0212320a3839bd4386312 Mon Sep 17 00:00:00 2001 From: Knee Cola Date: Wed, 31 Dec 2025 10:37:14 +0100 Subject: [PATCH] feat: add language selector for tenant notification emails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added ability to select the language for automatic notification emails sent to tenants. Users can choose between Croatian (hr) and English (en). If not set, defaults to the current UI language. Changes: - Add tenantEmailLanguage field to BillingLocation type (shared-code) - Add language selector fieldset in LocationEditForm below email settings - Add Zod validation for tenantEmailLanguage in locationActions - Include field in all database insert and update operations - Default to current locale if not explicitly set - Add translation labels for language selector (EN/HR) This allows tenants to receive bills and notifications in their preferred language regardless of the landlord's UI language preference. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- shared-code/src/db-types.ts | 2 ++ web-app/app/lib/actions/locationActions.ts | 8 ++++++++ web-app/app/ui/LocationEditForm.tsx | 18 ++++++++++++++++++ web-app/messages/en.json | 4 ++++ web-app/messages/hr.json | 4 ++++ 5 files changed, 36 insertions(+) diff --git a/shared-code/src/db-types.ts b/shared-code/src/db-types.ts index 54925b5..38da6bf 100644 --- a/shared-code/src/db-types.ts +++ b/shared-code/src/db-types.ts @@ -81,6 +81,8 @@ export interface BillingLocation { tenantEmail?: string | null; /** (optional) tenant email status */ tenantEmailStatus?: EmailStatus | null; + /** (optional) language for tenant notification emails */ + tenantEmailLanguage?: "hr" | "en" | null; /** (optional) whether to automatically notify tenant */ billFwdEnabled?: boolean | null; /** (optional) bill forwarding strategy */ diff --git a/web-app/app/lib/actions/locationActions.ts b/web-app/app/lib/actions/locationActions.ts index f9f128a..be5da4c 100644 --- a/web-app/app/lib/actions/locationActions.ts +++ b/web-app/app/lib/actions/locationActions.ts @@ -47,6 +47,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ billFwdEnabled: z.boolean().optional().nullable(), tenantEmail: z.string().email(t("tenant-email-invalid")).optional().or(z.literal("")).nullable(), tenantEmailStatus: z.enum([EmailStatus.Unverified, EmailStatus.VerificationPending, EmailStatus.Verified, EmailStatus.Unsubscribed]).optional().nullable(), + tenantEmailLanguage: z.enum(["hr", "en"]).optional().nullable(), billFwdStrategy: z.enum(["when-payed", "when-attached"]).optional().nullable(), rentDueNotificationEnabled: z.boolean().optional().nullable(), rentDueDay: z.coerce.number().min(1).max(31).optional().nullable(), @@ -136,6 +137,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat billFwdEnabled: formData.get('billFwdEnabled') === 'on', tenantEmail: formData.get('tenantEmail') || null, tenantEmailStatus: formData.get('tenantEmailStatus') as "unverified" | "verification-pending" | "verified" | "unsubscribed" | undefined, + tenantEmailLanguage: formData.get('tenantEmailLanguage') as "hr" | "en" | undefined, billFwdStrategy: formData.get('billFwdStrategy') as "when-payed" | "when-attached" | undefined, rentDueNotificationEnabled: formData.get('rentDueNotificationEnabled') === 'on', rentDueDay: formData.get('rentDueDay') || null, @@ -162,6 +164,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat billFwdEnabled, tenantEmail, tenantEmailStatus, + tenantEmailLanguage, billFwdStrategy, rentDueNotificationEnabled, rentDueDay, @@ -220,6 +223,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat billFwdEnabled: billFwdEnabled || false, tenantEmail: tenantEmail || null, tenantEmailStatus: finalEmailStatus, + tenantEmailLanguage: tenantEmailLanguage || null, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotificationEnabled: rentDueNotificationEnabled || false, rentDueDay: rentDueDay || null, @@ -252,6 +256,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat billFwdEnabled: billFwdEnabled || false, tenantEmail: tenantEmail || null, tenantEmailStatus: finalEmailStatus, + tenantEmailLanguage: tenantEmailLanguage || null, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotificationEnabled: rentDueNotificationEnabled || false, rentDueDay: rentDueDay || null, @@ -277,6 +282,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat billFwdEnabled: billFwdEnabled || false, tenantEmail: tenantEmail || null, tenantEmailStatus: finalEmailStatus, + tenantEmailLanguage: tenantEmailLanguage || null, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotificationEnabled: rentDueNotificationEnabled || false, rentDueDay: rentDueDay || null, @@ -301,6 +307,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat billFwdEnabled: billFwdEnabled || false, tenantEmail: tenantEmail || null, tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, + tenantEmailLanguage: tenantEmailLanguage || null, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotificationEnabled: rentDueNotificationEnabled || false, rentDueDay: rentDueDay || null, @@ -377,6 +384,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat billFwdEnabled: billFwdEnabled || false, tenantEmail: tenantEmail || null, tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, + tenantEmailLanguage: tenantEmailLanguage || null, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotificationEnabled: rentDueNotificationEnabled || false, rentDueDay: rentDueDay || null, diff --git a/web-app/app/ui/LocationEditForm.tsx b/web-app/app/ui/LocationEditForm.tsx index 769dab6..b3cef2f 100644 --- a/web-app/app/ui/LocationEditForm.tsx +++ b/web-app/app/ui/LocationEditForm.tsx @@ -42,6 +42,7 @@ export const LocationEditForm: FC = ({ location, yearMont tenantTown: location?.tenantTown ?? "", tenantEmail: location?.tenantEmail ?? "", tenantEmailStatus: location?.tenantEmailStatus ?? EmailStatus.Unverified, + tenantEmailLanguage: location?.tenantEmailLanguage ?? (locale as "hr" | "en"), tenantPaymentMethod: location?.tenantPaymentMethod ?? "none", proofOfPaymentType: location?.proofOfPaymentType ?? "none", billFwdEnabled: location?.billFwdEnabled ?? false, @@ -426,6 +427,23 @@ export const LocationEditForm: FC = ({ location, yearMont )} +
+ {t("notification-language-legend")} + + +
+
{t("scope-legend")} {!location ? ( diff --git a/web-app/messages/en.json b/web-app/messages/en.json index c7fca13..d91a1cf 100644 --- a/web-app/messages/en.json +++ b/web-app/messages/en.json @@ -205,6 +205,10 @@ "verified": "this e-mail address has been verified", "unsubscribed": "tenant unsubscribed this address from receiving emails" }, + "notification-language-legend": "NOTIFICATION EMAIL LANGUAGE", + "notification-language-label": "Language for automatic notification emails", + "notification-language-option-hr": "Croatian (Hrvatski)", + "notification-language-option-en": "English", "warning-missing-tenant-names": "Warning: Tenant first and last name are missing. The 2D barcode will not be displayed to the tenant when they open the shared link until both fields are filled in.", "save-button": "Save", "cancel-button": "Cancel", diff --git a/web-app/messages/hr.json b/web-app/messages/hr.json index fd97d68..0c238e4 100644 --- a/web-app/messages/hr.json +++ b/web-app/messages/hr.json @@ -204,6 +204,10 @@ "verified": "podstanar je potvrdio ovu e-mail adresu", "unsubscribed": "podstanar je odjavio ovu e-mail adresu" }, + "notification-language-legend": "JEZIK ZA OBAVIJESTI", + "notification-language-label": "Jezik za automatske obavijesti putem e-maila", + "notification-language-option-hr": "Hrvatski", + "notification-language-option-en": "Engleski (English)", "warning-missing-tenant-names": "Upozorenje: Ime i prezime podstanara nedostaju. 2D barkod neće biti prikazan podstanaru kada otvori podijeljenu poveznicu dok oba polja ne budu popunjena.", "save-button": "Spremi", "cancel-button": "Odbaci",