i18n — paraglide¶
@inlang/paraglide-jsis the only i18n inbricksoffice-projects.messages/fr.jsonis the source;src/core/i18n/paraglide/is generated and.gitignored. Keep in sync: changing paraglide setup, message structure, key-naming convention, or BO compile flow → update this file in the same PR.
Why paraglide (not react-i18next like mobile / front-app)? BO is single-locale (fr) and TS-strict — paraglide compiles each key into a typed function so misspellings and arg mismatches fail at typecheck. Smaller bundle (one fn per used key, no runtime resolver). Mobile/web stay on react-i18next because they need multi-locale + runtime switching.
Hard rules¶
- Every user-visible string through paraglide. JSX,
title,aria-label,placeholder, toasts, validation errors, confirmations. - One i18n system. No
react-i18next,react-intl, or ad-hocconst labels = { …: 'Texte FR' }with raw French. Exception: a typed wire→paraglide bridge (Record<ApiWireValue, () => m[…]()>) for a closed API enum — paraglide stays the string source; the map only dispatches (canon:translateEcheancierEventType.ts). - Single source:
messages/fr.json. Never edit anything undersrc/core/i18n/paraglide/— generated. baseLocale: 'fr', only localefr. Don't add a second locale preemptively.- No
+concatenation around translations. Word order can't be translated — interpolate. - ❌
m.greeting() + ' ' + name✅m.greeting({ name }) - Bracket access for dotted keys. Paraglide v2 exports nested JSON keys as dotted-string identifiers (
m['auth.login.submit']()), NOT snake_case. The snake_case form compiles but resolves toundefinedat runtime — silent breakage. Grepparaglide/messages/_index.jsif unsure.
Usage¶
import { m } from '@core/i18n'
<h1>{m.home_welcome()}</h1>
<p>{m.home_edit_hint({ file: 'src/routes/index.tsx' })}</p>
<span>{m['account.theme.light']()}</span> {/* nested → bracket access */}
Adding a message¶
- Add
"key_name": "Texte FR avec {var}"tomessages/fr.json(alphabetical-ish, grouped by feature). - Vite HMR regenerates on the next request. One-shot:
pnpm buildor restartpnpm dev. - Call as
m.key_name({ var }).
Key naming¶
snake_case, prefixed by feature/area:projects_list_empty_state,auth_login_invalid_credentials.- Group by domain, not by component path. Same key reusable when meaning is identical.
- Keys stable. New meaning → new key, don't overload.
Where copy lives¶
- App-specific:
messages/fr.jsonhere. - Shared across
bricksoffice-*: lift into BO's ownmessages/fr.json. - API error codes:
projects/common/both/api-communication-bricksoffice/src/translations/fr.jsonundererror-api.<code>— see data-fetching.
BO has its own paraglide instance¶
After editing BO's messages/fr.json:
Commit the regenerated src/i18n/paraglide/. BO components access m via its own import — don't pass message functions through props.
Dates and numbers (not via paraglide)¶
- Dates:
formatDate/formatDateTimefrom@bricks-common/bo/core. - Currency:
formatCentsFull/formatCentsShort. NeverIntl.NumberFormat. - Missing helper → lift one into BO core.
What NOT to do¶
- ❌
m.auth_login_submit()for a nested key — bracket access (m['auth.login.submit']()). - ❌ Translate an API error code in the consumer's
messages/fr.json— lives inapi-communication-bricksoffice. - ❌ Edit
src/core/i18n/paraglide/— generated.