Multi-Language Support

Configure language options for your merchants and their customers.

Supported Languages

JoPay supports four languages across the payment UI:

CodeLanguage
enEnglish
esSpanish
frFrench
ptPortuguese

Partner Configuration

Each partner record has three language-related fields:

  • ui_primary_language — the default language for all merchant and payment UIs under this partner. Required.
  • ui_secondary_language — an optional second language. When set, users see a language switcher in the UI.
  • ui_enabled_languages — the full list of languages available for this partner. Only languages in this list can be selected by users. Must be a subset of ["en", "es", "fr", "pt"].
All translation packs are bundled in the app. A language is only available if its translation pack is complete (every key has a non-empty translation). JoPay validates this at runtime using the packComplete() check.

How Language Selection Works

Language preference is stored in a cookie called p2ppay_lang. Here is the flow:

  1. Default — when a user first visits, the UI renders in the partner's ui_primary_language.
  2. Language switcher — if the partner has more than one enabled language, a language selector appears in the UI. The user picks their preferred language.
  3. Form submission — selecting a language submits a POST /api/lang request with the chosen language code, a CSRF token, and a return URL.
  4. Validation — the server checks that the selected language is in the partner's ui_enabled_languages list and that the translation pack is complete. If either check fails, the request is rejected.
  5. Cookie set — on success, the server sets the p2ppay_lang cookie (HTTP-only, same-site strict, secure in production) and redirects back to the return URL.
  6. Subsequent requests — all pages read the p2ppay_lang cookie and render in the selected language.

Translation Architecture

Translations are organized as flat JSON files in the messages/ directory:

  • messages/en.json — English (baseline)
  • messages/es.json — Spanish
  • messages/fr.json — French
  • messages/pt.json — Portuguese

The i18n.ts module flattens nested JSON into dot-separated keys (e.g. dashboard.title) and provides a getMessage(lang, key) function that falls back to the English translation if a key is missing in the selected language.

Best Practices

  • Keep enabled languages minimal — only enable languages that your merchants and customers actually need. Each language adds a switcher option and requires complete translations.
  • Test the payment page — open a payment link and switch languages to verify that all labels, buttons, and error messages are correctly translated.
  • English fallback — if a translation key is missing in a non-English pack, JoPay falls back to English. This ensures the UI never shows raw keys to users.
The language cookie is scoped to the domain. If your partner uses a custom domain, the cookie is set on that domain. Users switching between different partner domains will have separate language preferences.