You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add internationalization (i18n) support (#164) (#1765)
* feat: add internationalization (i18n) support (#164)
Introduces full i18n support using next-intl with English (en) and German
(de) catalogs, persistent per-user locale preference, locale-aware
formatting, and a standardised server-side error code system so messages
can be translated client-side.
Highlights:
- next-intl integration with deep-merged English fallback for missing keys
- LanguageSwitcher component + setLocale GraphQL mutation
- bm_web_users.locale column + persisting/loading user preference
- ExposedError now carries stable code + meta; surfaced via GraphQL
formatError and Koa error middleware (BAD_USER_INPUT messages pass
through verbatim per the documented out-of-scope policy)
- Locale-aware date formatting via dynamic date-fns locale loading with
promise dedupe and eager default-locale preload
- Localised react-select and @nateradebaugh/react-datetime widgets
- Comprehensive message extraction across components, pages, notifications
- New server/data/error-translation.js shared by REST + GraphQL helpers
- Added unit tests: error-translation, formatError, locale helpers,
setLocale mutation, me.locale field
- Documentation: README + CONTRIBUTING updates covering localisation
workflow, adding new locales, and ExposedError compatibility note
* fix(i18n): preserve password length detail via INVALID_PASSWORD_LENGTH
The Cypress login error test expects "Invalid password, minimum length 6
characters", but with i18n the client looks up the error code in the
translation catalog. The shared INVALID_PASSWORD code mapped to the
generic "Password is invalid", losing the actionable length detail.
Split into two codes so the message stays specific without baking the
magic number into translations:
- INVALID_PASSWORD: kept generic ("Password is invalid"), used for the
unreachable type-mismatch case.
- INVALID_PASSWORD_LENGTH: parameterised via meta.minLength so the
translated string ("Invalid password, minimum length {minLength}
characters") matches the original UX.
Also exercises the meta pass-through end-to-end (Koa REST + GraphQL),
proving the translation pipeline works for parameterised messages.
Copy file name to clipboardExpand all lines: CONTRIBUTING.md
+74Lines changed: 74 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,3 +20,77 @@ In order for us to help you please check that you've completed the following ste
20
20
* Develop in a topic branch, not master (e.g. `feature/new-view`)
21
21
* Write a convincing description of your PR and why we should land it
22
22
* If you write JavaScript, make sure you follow https://standardjs.com/
23
+
24
+
## Localisation
25
+
26
+
The UI uses [`next-intl`](https://next-intl.dev/) for client-side translations. Translation catalogues live in [`messages/`](messages/) and the locale registry / negotiation logic lives in [`server/data/locales.js`](server/data/locales.js) (re-exported by [`utils/locale.js`](utils/locale.js) for client code).
27
+
28
+
### Authoring rules
29
+
30
+
* Always introduce new user-facing text via `useTranslations(...)` (or `t.rich(...)` when the message contains markup or interpolated React elements). Never hard-code English strings in components.
31
+
* Group keys by feature area: `common.*`, `nav.*`, `forms.*`, `pages.<page>.*`, `errors.*`, `widgets.*`, `notifications.*`, `components.*`.
32
+
* Use ICU MessageFormat for plurals/interpolation: `"Removed {count, plural, one {# item} other {# items}}"`.
33
+
* Keep `messages/en.json` as the canonical source of truth. Every other locale must mirror its key structure exactly.
34
+
35
+
### Adding a string
36
+
37
+
1. Add the key + English copy to `messages/en.json`.
38
+
2. 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.
39
+
3. Reference the key from a component:
40
+
```js
41
+
import { useTranslations } from'next-intl'
42
+
43
+
constt=useTranslations('pages.login')
44
+
return<h1>{t('title')}</h1>
45
+
```
46
+
47
+
### Adding a new locale
48
+
49
+
1. Add the locale code to `SUPPORTED_LOCALES` in `server/data/locales.js`.
50
+
2. Extend `LOCALE_CONFIG` in `utils/locale.js` with `label` (native name shown in the switcher), `htmlLang`, `openGraphLocale`, `dateFormat`, and `dateFnsLocale` (must match a [`date-fns` locale module](https://date-fns.org/v4.1.0/docs/I18n)).
51
+
3. Add the locale entry to `dateFnsLocaleLoaders` in `utils/format-distance.js` so dynamic imports work.
52
+
4. Create `messages/<locale>.json` with a 1:1 copy of `messages/en.json`, then translate.
53
+
5. Run `npm run build` and `npm run test` to verify the locale loads and tests pass.
54
+
55
+
The `LanguageSwitcher` component automatically picks up new locales from `SUPPORTED_LOCALES` and renders them using the `label` from `LOCALE_CONFIG`.
56
+
57
+
### Server-side error messages
58
+
59
+
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.
60
+
61
+
* Always pass an error code as the second argument to `ExposedError`:
62
+
```js
63
+
thrownewExposedError('Server not found', 'SERVER_NOT_FOUND')
64
+
```
65
+
* For dynamic content (e.g. plurals, names), pass a `meta` object as the third argument:
* In Koa REST routes, call `ctx.throw(status, message, { code: 'STABLE_CODE', meta: {...} })` so the JSON body exposes `code` and `meta`. The `code` and `meta` properties are surfaced by the error middleware in [`server/app.js`](server/app.js).
70
+
* Add a corresponding key to `messages/<locale>.json` under `errors.<CODE>` for every supported locale. Use ICU placeholders matching the `meta` keys.
71
+
* The client UI (`components/ErrorMessages.js`/`utils/locale.js#translateGraphqlError` for GraphQL, `utils/locale.js#translateRestError` for REST) looks up the code and falls back to the server's message if no translation exists.
72
+
*`BAD_USER_INPUT` errors (raised by `graphql-constraint-directive`) are intentionally **not** assigned an `appCode`, so the constraint message is shown verbatim. See the out-of-scope list below.
73
+
74
+
> **Compatibility note**: `ExposedError` now defaults `code` to `'UNKNOWN'` (and `extensions.appCode` to `'UNKNOWN'`) when no second argument is supplied. Previously this field was `undefined`. Any downstream consumer that checks `if (!err.code)` should be updated to check for the literal string `'UNKNOWN'` instead. New call-sites must always pass an explicit, stable code.
75
+
76
+
### Out-of-scope (explicitly NOT translated)
77
+
78
+
The following surfaces are intentionally left in English to keep the scope manageable:
79
+
80
+
* The setup/installer SPA under `server/setup/static/` (run-once flow).
Copy file name to clipboardExpand all lines: README.md
+14-1Lines changed: 14 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -218,9 +218,22 @@ npm run test
218
218
npm run cypress
219
219
```
220
220
221
+
## Localisation
222
+
223
+
The UI is localised with [`next-intl`](https://next-intl.dev/). Currently shipping languages: **English (`en`)** and **German (`de`)**.
224
+
225
+
For details on adding a new language, translating new strings, or wiring up server-side error codes, see [`CONTRIBUTING.md`](CONTRIBUTING.md#localisation).
2.`bm_locale` cookie (set by the in-app language switcher)
231
+
3.`Accept-Language` HTTP header
232
+
4. Fallback to `en`
233
+
221
234
## Contributing
222
235
223
-
If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome.
236
+
If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for guidelines on translations, error codes, and other development conventions.
0 commit comments