Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,58 @@
import '@testing-library/jest-dom';
import enMessages from './messages/en.json';

// next-intl ships as ESM and trips Jest's default transform. Stub the hooks
// so tests resolve real strings from messages/en.json (preserving the assertions
// that query rendered output by English text), while skipping the real ESM bundle.
type Messages = Record<string, Record<string, string>>;
const messages = enMessages as Messages;

const interpolate = (template: string, params?: Record<string, unknown>) => {
if (!params) return template;
// Strip ICU plural blocks so we at least render a stable string in tests.
let result = template.replace(
/\{(\w+),\s*plural,([^}]|\{[^}]*\})*\}/g,
(_, key: string) => {
const value = params[key];
return value === undefined ? `{${key}}` : String(value);
}
);
result = result.replace(/\{(\w+)\}/g, (_, key: string) => {
const value = params[key];
return value === undefined ? `{${key}}` : String(value);
});
return result;
};

const resolve = (namespace: string | undefined, key: string): string => {
if (namespace && messages[namespace] && messages[namespace][key] !== undefined) {
return messages[namespace][key];
}
return key;
};

jest.mock('next-intl', () => ({
useTranslations: (namespace?: string) =>
(key: string, params?: Record<string, unknown>) =>
interpolate(resolve(namespace, key), params),
useLocale: () => 'en',
useFormatter: () => ({
number: (v: number) => String(v),
dateTime: (v: Date) => v.toISOString(),
}),
NextIntlClientProvider: ({children}: {children: React.ReactNode}) => children,
}));

jest.mock('next-intl/server', () => ({
getLocale: async () => 'en',
getMessages: async () => messages,
getTranslations: async (namespace?: string) =>
(key: string, params?: Record<string, unknown>) =>
interpolate(resolve(namespace, key), params),
}));

// `@/i18n/actions` pulls in `next/cache`, which ships as ESM and breaks
// Jest's transform. Tests don't exercise the locale-cookie action, so stub it.
jest.mock('@/i18n/actions', () => ({
setLocaleCookie: jest.fn(async () => undefined),
}));
217 changes: 217 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
{
"Metadata": {
"title": "Motzkin Store · School Equipment",
"description": "Order the school supply list for your child in Kiryat Motzkin."
},
"Header": {
"brand": "Motzkin Store",
"tagline": "City of Kiryat Motzkin",
"home": "Home",
"about": "About",
"contact": "Contact",
"cart": "Cart",
"cartAriaLabel": "Cart, {count} {list}",
"cartListSingular": "list",
"cartListPlural": "lists",
"signOut": "Sign out",
"openMenu": "Open menu"
},
"Footer": {
"brand": "Motzkin Store",
"tagline": "City of Kiryat Motzkin",
"description": "Order school equipment for children attending schools in Kiryat Motzkin.",
"siteHeading": "Site",
"orderEquipment": "Order equipment",
"cart": "Cart",
"about": "About",
"contact": "Contact",
"helpHeading": "Help",
"serviceCenter": "Service centre",
"quickDial": "Quick dial",
"copyright": "© {year} Motzkin Store · Built for the<highlight> City of Kiryat Motzkin</highlight>.",
"privacy": "Privacy policy",
"terms": "Terms of service"
},
"LanguageSwitcher": {
"label": "Language",
"english": "English",
"hebrew": "עברית"
},
"Home": {
"eyebrow": "School supplies",
"headline": "Order your child's<br></br><accent>school list</accent><trail>, in one go.</trail>",
"subtitle": "Pick your school and your child's grade to see the equipment list, then check out in one payment.",
"stepSchool": "School",
"stepGrade": "Grade",
"stepItems": "Items",
"schoolLabel": "School",
"schoolPlaceholder": "Search for a school",
"schoolLoading": "Loading schools…",
"schoolHint": "Start by selecting the school your child attends.",
"gradeLabel": "Grade",
"gradePlaceholder": "Search for a grade",
"gradeSelectSchoolFirst": "Select a school first",
"gradeHint": "Pick the grade your child is starting.",
"loading": "Loading…",
"secretariatNotice": "If something on the list looks wrong, please check with the school secretariat first."
},
"Login": {
"brand": "Motzkin Store",
"tagline": "City of Kiryat Motzkin",
"eyebrow": "Parent sign-in",
"title": "Login",
"intro": "Sign in to access this year's equipment lists and complete your order.",
"usernameLabel": "Username",
"usernamePlaceholder": "e.g. parent.name",
"passwordLabel": "Password",
"passwordPlaceholder": "••••••••",
"show": "Show",
"hide": "Hide",
"submit": "Login",
"submitting": "Signing in…",
"failed": "Failed to login. Please try again.",
"trouble": "Trouble signing in? Call the municipal service centre at <phone>04-878-0900</phone> or dial <quickDial>*5470</quickDial>."
},
"EquipmentList": {
"eyebrow": "Equipment list",
"headerItem": "Item",
"headerPrice": "Price",
"headerQuantity": "Qty",
"selectedSummary": "{selected} of {total} selected",
"selectAria": "Select {name}",
"deselectAria": "Deselect {name}",
"decreaseQty": "Decrease quantity",
"increaseQty": "Increase quantity",
"quantityAria": "Quantity for {name}",
"listSubtotal": "List subtotal"
},
"SaveToCart": {
"idle": "Save list to cart",
"saved": "Saved to cart",
"itemCount": "· {count, plural, one {# item} other {# items}}",
"toast": "List saved to cart"
},
"ConfirmDialog": {
"confirm": "Confirm",
"cancel": "Cancel"
},
"Cart": {
"stepIndicator": "Step 2 of 3",
"title": "Your cart",
"clearAll": "Clear all",
"emptyTitle": "Your cart is empty",
"emptySubtitle": "Once you save a school's equipment list, it will appear here ready for checkout.",
"browseEquipment": "Browse equipment lists",
"statLists": "Lists",
"statItems": "Items",
"statTotal": "Total",
"justNow": "just now",
"addedAt": "Added {date}",
"removeAriaLabel": "Remove this list from cart",
"removeTitle": "Remove from cart",
"itemColumn": "Item",
"qtyColumn": "Qty",
"unitColumn": "Unit",
"totalColumn": "Total",
"listSubtotal": "List subtotal",
"checkoutNotice": "Ready to complete your order? You'll be redirected to a secure payment page.",
"orderTotal": "Order total",
"checkout": "Proceed to checkout",
"dialogClearTitle": "Clear entire cart?",
"dialogClearMessage": "All saved equipment lists will be removed from your cart. This cannot be undone.",
"dialogClearConfirm": "Clear all",
"dialogRemoveTitle": "Remove this list?",
"dialogRemoveMessage": "“{name}” will be removed from your cart.",
"dialogRemoveConfirm": "Remove",
"dialogKeep": "Keep"
},
"Checkout": {
"stepIndicator": "Step 3 of 3",
"title": "Review & pay",
"reviewIntro": "Please review the lists below. Payment is handled by our secure payment provider. No card details are stored on this site.",
"emptyTitle": "Nothing to check out",
"emptySubtitle": "Your cart is empty. Add at least one equipment list before continuing.",
"goToCart": "Go to cart",
"statLists": "Lists",
"statItems": "Items",
"statTotal": "Total",
"itemColumn": "Item",
"qtyColumn": "Qty",
"unitColumn": "Unit",
"totalColumn": "Total",
"listSubtotal": "List subtotal",
"securePayment": "Secure payment via Stripe",
"amountToPay": "Amount to pay",
"backToCart": "← Back to cart",
"confirmAndPay": "Confirm & pay {amount}",
"redirecting": "Redirecting to payment…",
"genericError": "Something went wrong. Please try again."
},
"MockPayment": {
"banner": "Mock Payment Gateway - Development Only",
"title": "Complete Payment",
"orderLabel": "Order: {id}",
"cardNumber": "Card Number",
"expiry": "Expiry",
"cvv": "CVV",
"pay": "Pay Now",
"processing": "Processing...",
"simulateFailure": "Simulate Failure",
"declined": "Payment declined by mock provider",
"loading": "Loading..."
},
"PaymentSuccess": {
"eyebrow": "Order confirmed",
"title": "Thank you. Your order is on its way.",
"clearedMessage": "Your payment was received and your cart has been cleared. You will receive a confirmation by email shortly.",
"clearingMessage": "Finalising your order...",
"invalidMessage": "We could not confirm your payment session, but your cart has not been touched. Please contact support if you were charged.",
"returnHome": "Return home",
"needHelp": "Need help?",
"loading": "Loading..."
},
"About": {
"eyebrow": "About",
"title": "About Motzkin Store",
"intro": "Motzkin Store is a school-supply ordering site for families in Kiryat Motzkin. Pick the school, pick the grade, and order the equipment list in one go.",
"teamEyebrow": "The team",
"teamHeading": "Built by students of the Technion",
"teamDescription": "Developed as part of the Project in Software Engineering course, in cooperation with the Kiryat Motzkin Municipality.",
"roleMentor": "Mentor",
"roleMember": "Team Member",
"teamMembersHeading": "Team members"
},
"Contact": {
"eyebrow": "Get in touch",
"title": "We're here to help.",
"subtitle": "For questions about school equipment orders, payments, or technical issues, reach out to the municipality's service centre or to the developer team.",
"municipalityEyebrow": "Municipality",
"municipalityHeading": "Kiryat Motzkin City Hall",
"address": "Address",
"addressLine1": "Lev Hakrayot Centre",
"addressLine2": "Ben Gurion Boulevard 80",
"addressLine3": "Kiryat Motzkin, Israel",
"serviceCenter": "Service centre",
"mainLine": "Main line",
"quickDial": "Quick dial",
"whatsapp": "WhatsApp",
"generalInquiries": "General enquiries",
"website": "Website",
"developmentEyebrow": "Development",
"devTeamHeading": "Site & technical support",
"devTeamIntro": "Found a bug or have a feature suggestion? Our development team handles it on GitHub.",
"devTeamFind": "Team profiles and direct links are on the About page.",
"viewTeam": "Meet the team →",
"officeHours": "Office hours",
"timezoneNote": "All times Israel Time (IST)",
"weekdays": "Sunday – Tuesday, Thursday",
"weekdaysHours": "8:00 – 15:30",
"wednesday": "Wednesday",
"wednesdayHours": "8:00 – 13:00\n16:00 – 18:00",
"weekend": "Friday – Saturday",
"weekendHours": "Closed"
},
"Common": {
"currencyILS": "{amount} ILS"
}
}
Loading
Loading