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¶
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.jsonto 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 thedocs/ci/find-tbd.shcheck 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 — checkUser.preferredLocaleinlibs/_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¶
- Routing:
http://localhost:3000/fr/casinoresolves and renders French copy. - Picker: the locale picker shows the new locale and switches correctly.
- No untranslated keys: grep
messages/fr.jsonfor English source strings — every value should be translated. If using{{TBD-translate}}markers, count must reach zero before launch. - Build:
pnpm lintandpnpm buildexit 0. - 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.jsonvalidity is purely shape-driven — there is no semantic check that the translation is correct. Quality gating is the customer's responsibility.- The
User.preferredLocalefield for backend-driven copy:{{verify before customer share}}— confirm by greppingpreferredLocaleinlibs/_prisma/src/schema/api.prismaandapps/api/src/user/. - The Russian-language
ebit-apiREADME is a development artefact, not a player-facing locale. Don't conflate.
Cross-links¶
integration-cookbook.md— index.customize-branding.md— branding overlap (copy review, email templates).docs/api-reference/index.md— API behaviour does not depend on locale; only outbound copy does.- next-intl docs: https://next-intl-docs.vercel.app/.