Aller au contenu

i18n — paraglide

@inlang/paraglide-js is the only i18n in bricksoffice-projects. messages/fr.json is 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-hoc const 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 under src/core/i18n/paraglide/ — generated.
  • baseLocale: 'fr', only locale fr. Don't add a second locale preemptively.
  • No + concatenation around translations. Word order can't be translated — interpolate.
  • m.greeting() + ' ' + namem.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 to undefined at runtime — silent breakage. Grep paraglide/messages/_index.js if 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

  1. Add "key_name": "Texte FR avec {var}" to messages/fr.json (alphabetical-ish, grouped by feature).
  2. Vite HMR regenerates on the next request. One-shot: pnpm build or restart pnpm dev.
  3. 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.json here.
  • Shared across bricksoffice-*: lift into BO's own messages/fr.json.
  • API error codes: projects/common/both/api-communication-bricksoffice/src/translations/fr.json under error-api.<code> — see data-fetching.

BO has its own paraglide instance

After editing BO's messages/fr.json:

pnpm --filter @bricks-common/bo i18n:compile

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 / formatDateTime from @bricks-common/bo/core.
  • Currency: formatCentsFull / formatCentsShort. Never Intl.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 in api-communication-bricksoffice.
  • ❌ Edit src/core/i18n/paraglide/ — generated.