We are more than happy to accept external contributions to the project in the form of feedback, bug reports and even better - pull requests :)
In order for us to help you please check that you've completed the following steps:
- Made sure you're bug isn't already fixed in the master branch.
- Used the search feature to ensure that the bug hasn't been reported before
- Included as much information about the bug as possible, including any output you've received, what Server OS, PHP version you're on, etc.
- Make sure you only submit one problem per issue
- Please check to make sure that there aren't existing pull requests attempting to address the issue mentioned.
- Non-trivial changes should be discussed in an issue first
- Develop in a topic branch, not master (e.g.
feature/new-view) - Write a convincing description of your PR and why we should land it
- If you write JavaScript, make sure you follow https://standardjs.com/
The UI uses next-intl for client-side translations. Translation catalogues live in messages/ and the locale registry / negotiation logic lives in server/data/locales.js (re-exported by utils/locale.js for client code).
- Always introduce new user-facing text via
useTranslations(...)(ort.rich(...)when the message contains markup or interpolated React elements). Never hard-code English strings in components. - Group keys by feature area:
common.*,nav.*,forms.*,pages.<page>.*,errors.*,widgets.*,notifications.*,components.*. - Use ICU MessageFormat for plurals/interpolation:
"Removed {count, plural, one {# item} other {# items}}". - Keep
messages/en.jsonas the canonical source of truth. Every other locale must mirror its key structure exactly.
- Add the key + English copy to
messages/en.json. - Add a translation under the same key in every other locale file (
messages/de.json, etc.). If a translation is unavailable at submission time, copy the English value as a placeholder so the key still resolves. - Reference the key from a component:
import { useTranslations } from 'next-intl' const t = useTranslations('pages.login') return <h1>{t('title')}</h1>
- Add the locale code to
SUPPORTED_LOCALESinserver/data/locales.js. - Extend
LOCALE_CONFIGinutils/locale.jswithlabel(native name shown in the switcher),htmlLang,openGraphLocale,dateFormat, anddateFnsLocale(must match adate-fnslocale module). - Add the locale entry to
dateFnsLocaleLoadersinutils/format-distance.jsso dynamic imports work. - Create
messages/<locale>.jsonwith a 1:1 copy ofmessages/en.json, then translate. - Run
npm run buildandnpm run testto verify the locale loads and tests pass.
The LanguageSwitcher component automatically picks up new locales from SUPPORTED_LOCALES and renders them using the label from LOCALE_CONFIG.
Errors thrown from GraphQL resolvers, GraphQL directives, REST routes, and webhook handlers are translated client-side via stable error codes — not by translating the server's English text.
- Always pass an error code as the second argument to
ExposedError:throw new ExposedError('Server not found', 'SERVER_NOT_FOUND')
- For dynamic content (e.g. plurals, names), pass a
metaobject as the third argument:throw new ExposedError('This password isn\'t safe…', 'PASSWORD_COMPROMISED', { count: 5 })
- In Koa REST routes, call
ctx.throw(status, message, { code: 'STABLE_CODE', meta: {...} })so the JSON body exposescodeandmeta. Thecodeandmetaproperties are surfaced by the error middleware inserver/app.js. - Add a corresponding key to
messages/<locale>.jsonundererrors.<CODE>for every supported locale. Use ICU placeholders matching themetakeys. - The client UI (
components/ErrorMessages.js/utils/locale.js#translateGraphqlErrorfor GraphQL,utils/locale.js#translateRestErrorfor REST) looks up the code and falls back to the server's message if no translation exists. BAD_USER_INPUTerrors (raised bygraphql-constraint-directive) are intentionally not assigned anappCode, so the constraint message is shown verbatim. See the out-of-scope list below.
Compatibility note:
ExposedErrornow defaultscodeto'UNKNOWN'(andextensions.appCodeto'UNKNOWN') when no second argument is supplied. Previously this field wasundefined. Any downstream consumer that checksif (!err.code)should be updated to check for the literal string'UNKNOWN'instead. New call-sites must always pass an explicit, stable code.
The following surfaces are intentionally left in English to keep the scope manageable:
- The setup/installer SPA under
server/setup/static/(run-once flow). - CLI command output (
cli/commands/**,cli/utils/**). graphql-constraint-directivevalidation messages.- User-generated content (player names, ban reasons, appeal/report comments, custom roles, server names, document content).
- Server logs and Pino output (
logger.*calls).
If you want to localise any of these, please open an issue first to discuss scope.
Before submitting a PR that touches localisation:
- Toggle the language switcher in the top-right and confirm the page re-renders in the chosen language.
- Reload the page and confirm the choice persisted via the
bm_localecookie. - If you're logged in, confirm
setLocaleran and the preference survives a logout/login cycle. - Check that
<html lang="…">updates to match (DevTools → Elements panel). - Trigger a known server error (e.g. submit invalid login) and verify the message renders in the active locale.