🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsWindows Authentication SMS not received or working(12.09.2026 um 11:54 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC(10.09.2026 um 20:11 Uhr)
🕵️ SicherheitslückenDefender 0-Day ShieldBreak (CVE-2026-69414) nicht sauber gepatcht - BornCity(11.09.2026 um 12:52 Uhr)
🪟 Windows TippsServertimeout in Outlook über 10 Minuten verlängern(12.09.2026 um 15:10 Uhr)
🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsWindows Authentication SMS not received or working(12.09.2026 um 11:54 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC(10.09.2026 um 20:11 Uhr)
🕵️ SicherheitslückenDefender 0-Day ShieldBreak (CVE-2026-69414) nicht sauber gepatcht - BornCity(11.09.2026 um 12:52 Uhr)
🪟 Windows TippsServertimeout in Outlook über 10 Minuten verlängern(12.09.2026 um 15:10 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 5 Min Lesezeit
0

Multilingual emails from a Stripe webhook — inferring language from currency

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

A subtle trap of taking a SaaS international: the system emails sent from your Stripe webhook. Purchase confirmation, renewal success, payment failure, plan change — four kinds of emails triggered by Stripe events. We recently discovered all four had been hardcoded to Japanese for months, sending Japanese receipts and failure notices to English-paying users overseas. The kind of bug that quietly persists forever unless you go looking.



This post walks through the currency-based language inference design we landed on, plus the small mb_language trap that nearly ruined the fix.






Where do you get "the user's language" from? — three options



There were essentially three design options for picking the email language inside the webhook:



Option A. Keep a language column in the DB. The classical answer — store language at signup, look it up when sending mail. But it requires a DB migration, leaves existing users in "language unknown" until backfill, and you still need fallback inference for the first email anyway



Option B. Pull from the Stripe API. stripe.Customer.retrieve() exposes preferred_locales. Authoritative in theory, but adds a round trip per send, and not every customer has it set



Option C. Infer from the Stripe event's currency field. Always present in the webhook payload (usd / jpy / etc.). No extra API call, no DB migration, applies to existing users immediately



We picked Option C, because:




  • Currency is a strong, fixed-at-purchase signal — it doesn't change later

  • It's already in the payload, so the cost is zero

  • Zero DB touches means low regression risk on deploy

  • Existing users get the new behavior automatically (no migration script)






Implementing lang_from_currency



The whole helper is this one function:




CODE
/** Infer display language from a Stripe event's currency. */
function lang_from_currency(string $currency): string {
$en_currencies = ['usd']; // USD → English user
return in_array(strtolower($currency), $en_currencies, true) ? 'en' : 'ja';
}






USD means English; everything else (including JPY) means Japanese. EUR or GBP can be added to $en_currencies later when those markets open.



It's a "good enough" approximation, but it works in practice: there are essentially no English speakers buying in JPY, and the rare Japanese speaker on a USD plan ends up with English emails — which is more correlated to their explicit purchase intent than something like a browser Accept-Language header.






Pulling currency from the four webhook events



The four Stripe events have slightly different payload shapes, so currency lives in slightly different places:




CODE
// 1. checkout.session.completed — purchase confirmation
$checkout_lang = lang_from_currency($session['currency'] ?? 'jpy');
send_license_email($email, $client_name, $key, $plan, $period, $checkout_lang);

// 2. invoice.payment_succeeded — renewal success
$renewal_lang = lang_from_currency($invoice['currency'] ?? 'jpy');
send_renewal_email($email, $client_name, $plan, $renewal_lang);

// 3. invoice.payment_failed — payment failure
$failed_lang = lang_from_currency($invoice['currency'] ?? 'jpy');
send_payment_failed_email($email, $client_name, $plan, $failed_lang);

// 4. customer.subscription.updated — plan change
// (currency reached via the subscription object)
$changed_lang = lang_from_currency($sub_currency);
send_plan_changed_email($email, $client_name, $old_plan, $new_plan, $changed_lang);






The ?? 'jpy' fallback is insurance: if a test event somehow lacks currency, the function returns ja rather than throwing.






The subject-encoding trap — mb_language('uni' or 'Japanese')



This is where it gets sneaky. PHP's mb_send_mail() encodes the subject according to whatever mb_language() is set to:




CODE
mb_language($lang === 'en' ? 'uni' : 'Japanese');








  • mb_language('Japanese') encodes the subject in ISO-2022-JP via MIME (JIS X 0208-based, Japan-specific)


  • mb_language('uni') encodes the subject in UTF-8 Base64 (strict RFC 2047 compliance)



Why does it matter? If you leave mb_language('Japanese') in place and send an English subject like "Your license key for WP Maintenance Manager", the subject gets ISO-2022-JP MIME-encoded — and Gmail and Outlook's spam scoring goes up. Non-Japanese mail clients see a strange-looking encoded subject for what should be plain English text.



The fix is simple — flip mb_language() based on $lang in all four mail functions:




CODE
function send_license_email($email, $client_name, $key, $plan, $period, $lang = 'ja') {
mb_language($lang === 'en' ? 'uni' : 'Japanese');
// ... subject / body below branch on $lang
}






mb_language('uni') is available since PHP 7.2 (RFC 2047 compliant Base64 UTF-8). If you're handling Stripe webhooks and going multilingual, this is more or less mandatory knowledge.






The win of a no-DB-migration design



The net of this round: the database was not touched. The entire change lives in one file, webhook.php, with one new helper function lang_from_currency. New users get the right language obviously, but existing users who bought months ago also get their next renewal email in the right language, automatically.



This is what a stateless event-driven architecture combined with a strong inference signal can buy you. A DB-state design would have required a backfill pass over historical users — and rows that got missed during backfill would have stayed wrong forever.






Closing — three patterns for webhook i18n



Three principles worth keeping from this round:





  1. If the event payload carries a strong signal, skip the DB. Currency is a fixed-at-purchase signal that's already in every Stripe event. Routing logic through that signal beats a DB-state design when the data is already available — fewer moving parts, less maintenance overhead


  2. Set mb_language('uni') for non-Japanese subjects. ISO-2022-JP MIME encoding of English subject lines is a quiet contributor to spam-score creep. If you support both languages, switch encoding based on the user's language


  3. For multilingual coverage, suspect "is anything still hardcoded?" first. "Four mail functions were all hardcoded to Japanese" is the kind of bug that only surfaces when someone goes looking. Make every Stripe-event-handling function take an explicit language parameter in its signature, so adding a new event forces the author to think about language



If your SaaS is international, "we've been sending Japanese emails to English-paying users" is the kind of failure that doesn't surface until you check. Inferring language from currency at the webhook layer is a one-file, one-helper pattern that closes most of those leaks at a remarkably low cost.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
The Gemini desktop app is now available for Windows
1 Quelle
Windows Authentication SMS not received or working
1 Quelle
Windows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Multilingual emails from a Stripe webhook — inferring language from currency

Thematisch verwandte Begriffe: Multilingual, emails, from, Stripe · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...