From c24d567ae4e595874f7074d03fc15ef23c94afae Mon Sep 17 00:00:00 2001 From: Gerhard Scheikl Date: Fri, 15 May 2026 16:08:19 +0200 Subject: [PATCH] fix(invoice): localize Shopify payment-gateway names on the PDF Customer reported that on the German invoice PDF the payment method showed up as 'Zahlart: Bank Deposit' while the order-confirmation page on the storefront localized it correctly to 'Bank\u00fc1berweisung'. Cause: Shopify's Admin GraphQL API only ever returns the *English* template name in 'Order.paymentGatewayNames', even when the shop / order locale is German \u2014 the localization happens client-side at checkout but is NOT exposed via the API. So the PDF and the storefront naturally diverge unless we mirror the translations ourselves. Fix: introduce a per-language 'paymentGatewayLabels' map on 'InvoiceStrings' covering the built-in Shopify manual-payment templates (Bank Deposit, Money Order, Cash on Delivery) plus the standard non-manual gateways (Shopify Payments, PayPal, Klarna, Sofort, Giropay, Bogus). 'prettifyGatewayName' now takes this map and looks up the normalized key (lowercased, separators collapsed), falling back to a title-cased rendering for unknown values. DE result: 'Zahlart: Bank\u00fc1berweisung', 'Manuelle Zahlung', 'Nachnahme'. EN result: unchanged. New smoke assertions verify the DE PDF now shows 'Manuelle Zahlung' for the AT B2B fixture's 'manual' gateway and that the raw English 'Manual' no longer appears next to the 'Zahlart' label. Note on other Shopify-sourced strings on the PDF: 'shippingLine.title' (e.g. 'Standard') is similarly merchant/locale-dependent, but unlike gateway names it's fully customizable per-shop in Shopify Admin and is not a fixed enum we can translate \u2014 left untouched pending an explicit report. Product titles, discount codes and addresses are likewise merchant-/customer-supplied and flow through verbatim by design. --- app/services/invoice/i18n.ts | 42 ++++++++++++++++++++ app/services/invoice/pdf/InvoiceDocument.tsx | 34 ++++++++-------- scripts/render-sample.ts | 8 ++++ 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/app/services/invoice/i18n.ts b/app/services/invoice/i18n.ts index c371c41..a956b64 100644 --- a/app/services/invoice/i18n.ts +++ b/app/services/invoice/i18n.ts @@ -68,6 +68,16 @@ export interface InvoiceStrings { /** Used as the meta-row label when the order is a local pickup. The row * value is then the pickup location name (e.g. "Lager Graz"). */ pickupLocationLabel: string; + /** Localized labels for Shopify's built-in payment-gateway names. The + * Admin GraphQL API only ever returns the *English* template name + * (e.g. "Bank Deposit") in `Order.paymentGatewayNames`, even when the + * storefront / order-confirmation page renders the localized variant + * ("Banküberweisung"). We mirror Shopify's checkout copy here so the + * printed PDF matches what the customer saw at checkout. Lookup is + * case-insensitive on the normalized key (lowercased, separators + * collapsed). Unknown gateways fall back to a title-cased rendering + * of the raw name. */ + paymentGatewayLabels: Record; } /** Status displayed for the order's payment, derived from Shopify's @@ -171,6 +181,23 @@ const de: InvoiceStrings = { discountCodeLabel: "Rabattcode", pickupLabel: "Abholung", pickupLocationLabel: "Abholort", + paymentGatewayLabels: { + // Built-in Shopify manual payment methods (template names). + "bank deposit": "Banküberweisung", + "bank transfer": "Banküberweisung", + "money order": "Postanweisung", + "cash on delivery": "Nachnahme", + "cash on delivery (cod)": "Nachnahme", + // Generic / technical gateways. + manual: "Manuelle Zahlung", + bogus: "Bogus (Test)", + "shopify payments": "Shopify Payments", + paypal: "PayPal", + "paypal express checkout": "PayPal", + klarna: "Klarna", + sofort: "Sofort", + giropay: "Giropay", + }, }; const en: InvoiceStrings = { @@ -241,6 +268,21 @@ const en: InvoiceStrings = { discountCodeLabel: "Discount code", pickupLabel: "Pick-up", pickupLocationLabel: "Pick-up location", + paymentGatewayLabels: { + "bank deposit": "Bank deposit", + "bank transfer": "Bank transfer", + "money order": "Money order", + "cash on delivery": "Cash on delivery", + "cash on delivery (cod)": "Cash on delivery (COD)", + manual: "Manual", + bogus: "Bogus (Test)", + "shopify payments": "Shopify Payments", + paypal: "PayPal", + "paypal express checkout": "PayPal", + klarna: "Klarna", + sofort: "Sofort", + giropay: "Giropay", + }, }; // Locale → invoice language. We only render in German (`de`) when the diff --git a/app/services/invoice/pdf/InvoiceDocument.tsx b/app/services/invoice/pdf/InvoiceDocument.tsx index 638105b..bed32d0 100644 --- a/app/services/invoice/pdf/InvoiceDocument.tsx +++ b/app/services/invoice/pdf/InvoiceDocument.tsx @@ -311,7 +311,7 @@ export function InvoiceDocument({ invoice }: DocProps) { {t.paymentMethodLabel} - {invoice.paymentGatewayNames.map(prettifyGatewayName).join(", ")} + {invoice.paymentGatewayNames.map((n) => prettifyGatewayName(n, t.paymentGatewayLabels)).join(", ")} )} @@ -643,24 +643,24 @@ function normaliseWebUrl(url: string): string { /** * Turn a Shopify payment-gateway machine name (e.g. `shopify_payments`, - * `manual`, `bogus`) into something a customer can read on the invoice. We - * keep this purely cosmetic — the underlying value is preserved for any - * downstream automation. + * `manual`, `bogus`) or a built-in manual-payment template name (e.g. + * `Bank Deposit`, `Money Order`) into the localized customer-facing label + * shown on the invoice. The Shopify Admin API only exposes English + * template names — see `InvoiceStrings.paymentGatewayLabels` for the + * rationale. + * + * Lookup is keyed on the *normalized* name (lowercased, separators + * collapsed). Unknown gateways fall back to a title-cased rendering + * of the raw name so we never silently print empty meta-rows. */ -function prettifyGatewayName(name: string): string { - const known: Record = { - manual: "Manual", - bogus: "Bogus (Test)", - shopify_payments: "Shopify Payments", - paypal: "PayPal", - cash_on_delivery: "Cash on delivery", - "cash-on-delivery": "Cash on delivery", - }; - const key = name.trim().toLowerCase(); - if (known[key]) return known[key]; - // Replace separators and title-case each word. +function prettifyGatewayName( + name: string, + labels: Record, +): string { + const key = name.trim().toLowerCase().replace(/[_\-]+/g, " ").replace(/\s+/g, " "); + if (labels[key]) return labels[key]; return key - .split(/[_\s-]+/) + .split(" ") .filter(Boolean) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); diff --git a/scripts/render-sample.ts b/scripts/render-sample.ts index 6eaae78..d45f7dc 100644 --- a/scripts/render-sample.ts +++ b/scripts/render-sample.ts @@ -554,6 +554,14 @@ async function main() { assert("DE PDF shows payment status row", deText.includes("Zahlstatus")); assert("DE PDF shows payment status value 'Offen' for PENDING", deText.includes("Offen")); assert("DE PDF shows payment method row", deText.includes("Zahlart")); + // The Shopify Admin GraphQL API returns the *English* template name for + // built-in manual payment gateways even on German-locale shops — we + // localize it ourselves via i18n.paymentGatewayLabels so the PDF matches + // what the customer saw on the order-confirmation page. + assert("DE PDF localizes 'manual' gateway to 'Manuelle Zahlung'", + deText.includes("Manuelle Zahlung")); + assert("DE PDF no longer shows raw English 'Manual' as gateway label", + !/Zahlart[\s\S]{0,20}Manual\b/.test(deText)); assert("EN PDF shows payment status row", enText.includes("Payment status")); assert("EN PDF shows payment status value 'Outstanding' for PENDING", enText.includes("Outstanding"));