Skip to content

Add a new language to dropbet

Goal: add a new locale to the player-facing site (ebit-fe). Audience: customer engineering team launching dropbet in a new language market. Time: 0.5–1 day code, plus 1–2 weeks translation turnaround on the customer side. Friction: low for the engineering side; the gating cost is translation quality.

What you'll change

Layer Path Action
next-intl routing ebit-fe/src/i18n/routing.tsx:7 Add the locale code to the locales array.
Translation file ebit-fe/messages/<locale>.json New file. Copy from en.json, translate values.
Locale request config ebit-fe/src/i18n/request.ts Verify the locale resolves.
Date / number / currency formatting (per-component) Use next-intl's formatters; do not hardcode.
RTL support (Arabic / Hebrew etc.) ebit-fe/src/app/[locale]/layout.tsx Set dir="rtl" conditionally; review Tailwind RTL.
Backend localised strings apps/api/src/... (email, SMS, KYC notifications) Localise outbound copy.
SendGrid templates SendGrid UI Optional per-locale template variants.

Canonical example

The current locales (ebit-fe/src/i18n/routing.tsx:7):

export const routing = defineRouting({
  locales: ['en', 'de'],
  defaultLocale: 'en',
  localePrefix: 'as-needed',
});

Translation files at ebit-fe/messages/en.json and ebit-fe/messages/de.json carry the same key tree. The default locale en is the source of truth — every other locale must mirror its key set.

Steps

1. Pick the locale code

Use BCP-47 (e.g. fr for French, pt-BR for Brazilian Portuguese, ar-SA for Saudi Arabic). Lower-case the language tag; capital-case the region. next-intl uses these verbatim as URL segments.

2. Add to the routing config

Edit ebit-fe/src/i18n/routing.tsx:7:

export const routing = defineRouting({
  locales: ['en', 'de', 'fr'],         // <-- add 'fr'
  defaultLocale: 'en',
  localePrefix: 'as-needed',
});

localePrefix: 'as-needed' means the default locale (en) does not get a URL prefix, but every other locale does (/fr/casino, /de/casino, etc.). Do not change this without a customer-side SEO review.

3. Add the translation file

cp ebit-fe/messages/en.json ebit-fe/messages/fr.json

Then translate every value. The key tree must remain identical — adding or omitting keys breaks the build under pnpm tsc if next-intl's strict mode is on, and breaks runtime if not.

For a fresh language:

  • Send messages/en.json to the customer's translation team with context (which key appears where — pair with screenshots).
  • Insist on a single signed-off file. Avoid round-tripping through Google Translate or untracked spreadsheets.
  • Mark untranslated keys with {{TBD-translate: <key>}} so the docs/ci/ find-tbd.sh check can flag them.

4. Verify request resolution

Open ebit-fe/src/i18n/request.ts and confirm the import path of message files is dynamic (typically a import(\../../messages/${locale}.json`)pattern). If the file uses an explicitif (locale === 'en')` switch, add the new branch.

5. RTL support (only if applicable)

For Arabic, Hebrew, Persian, Urdu, etc., add a runtime check for direction:

// ebit-fe/src/app/[locale]/layout.tsx
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur']);
const dir = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr';
return <html lang={locale} dir={dir}></html>

Audit Tailwind classes that hardcode left-*, right-*, pl-*, pr-*, text-left, etc. — flip to logical equivalents (start-*, end-*, ps-*, pe-*, text-start). The Tailwind 3.3+ logical-property utilities handle most cases.

Visual QA every page in the RTL flow before shipping. RTL bugs hide easily.

6. Date / number / currency formatting

Use next-intl's useFormatter() everywhere. Do not call toLocaleString directly — useFormatter reads the active locale from context.

const format = useFormatter();
format.number(1234.5, { style: 'currency', currency: 'EUR' });
format.dateTime(new Date(), { dateStyle: 'medium' });

If the customer's market uses a non-default decimal separator or thousand-separator (e.g. 1.234,5 in many EU markets), next-intl handles this via the locale itself — verify with a screenshot once the locale is wired.

7. Backend localised strings

The backend ships some user-visible copy:

  • Email templates (SendGrid — see customize-branding.md §6).
  • KYC rejection reasons (currently English-only — {{verify with KYC team}} whether localised translations are required for the new market).
  • SMS / push notification copy (if enabled).

For each, decide:

  • Customer translates copy in SendGrid — add a per-locale template id to Doppler (SENDGRID_VERIFY_EMAIL_TEMPLATE_ID_FR, etc.). Backend selects the right template id by locale. Verify the backend has a "user's preferred locale" field — check User.preferredLocale in libs/_prisma/src/schema/api.prisma.
  • Customer accepts English-only emails — no engineering work for v1.

8. Production locale picker

The locale picker is a UI component in ebit-fe/src/components/ (search useLocale or next-intl/navigation). Confirm:

  • The new locale appears in the picker list.
  • The locale label is rendered in its native form ("Français" not "French").
  • The flag / icon is supplied (asset goes into ebit-fe/public/).

9. Build checks

cd ebit-fe
pnpm tsc          # type-check; will fail if message keys diverge under strict mode
pnpm lint         # next lint --max-warnings=0
pnpm build        # next build
pnpm dev          # smoke-check at /fr (or /<locale>)

pnpm lint runs both next lint and tsc --noEmit; both must exit 0. Treat warnings as build breakers.

10. Visual + functional QA

  • Top-level routes (/, /casino, /games/originals/dice, /account, /promotions, /leaderboard, /legal/{tos,pp}, /faq).
  • Sign-up + sign-in flows in the new locale.
  • A complete deposit flow (provider redirect copy + confirmation email).
  • A KYC submission flow (Sumsub iframe localisation — see Sumsub docs for supported languages).
  • Error states (invalid form input, network errors).

Verification

  1. Routing: http://localhost:3000/fr/casino resolves and renders French copy.
  2. Picker: the locale picker shows the new locale and switches correctly.
  3. No untranslated keys: grep messages/fr.json for English source strings — every value should be translated. If using {{TBD-translate}} markers, count must reach zero before launch.
  4. Build: pnpm lint and pnpm build exit 0.
  5. Email: sign up with a fr-preferring user; the verification email body matches the localised template (or English fallback if not yet localised).

Notes / known gaps

  • messages/fr.json validity is purely shape-driven — there is no semantic check that the translation is correct. Quality gating is the customer's responsibility.
  • The User.preferredLocale field for backend-driven copy: {{verify before customer share}} — confirm by grepping preferredLocale in libs/_prisma/src/schema/api.prisma and apps/api/src/user/.
  • The Russian-language ebit-api README is a development artefact, not a player-facing locale. Don't conflate.