The backend translates very little. Most text a user sees is rendered by the web app or the mobile app from their own translation files. The server owns text in a few specific places, and each has its own way of choosing a language.

What is translated where

@repo/i18n

packages/i18n holds the locale config and the translation files that the server needs.
Six locales are supported: he, en, ar, de, es, fr. Hebrew and Arabic are right to left.

Translation files

saas.json is in this package because the package was ported from the web app. The server reads the mail scope. The web app has its own copy of these files in its own repo.

How messages are loaded

getMessagesForLocale(locale, scope):
  1. Reads <locale>/<scope>.json and <locale>/shared.json from disk and merges them.
  2. If the locale is not the default, loads the same two files for the default locale (he) and merges the requested locale on top.
So a key missing from a non default locale falls back to Hebrew, not English. A missing file is treated as an empty object. Files are read with fs relative to the compiled module (import.meta.url). A dynamic import() of JSON did not survive bundling. The build script copies the files:
After editing a translation file, rebuild @repo/i18n, or the running API keeps the old text.

How locale is resolved

There is no locale middleware on /v1. The domain API does not read Accept-Language or any locale header. Studio.locale defaults to he and Studio.timezone to Asia/Jerusalem in the schema. User.locale is optional.

Push notification text

Push text is configuration, not translation. Each studio edits its own titles and bodies in the notification settings, and those strings are stored in StudioNotificationConfig. The fallbacks in NOTIFICATION_CONFIG_DEFAULTS are written in Hebrew. A studio that works in another language changes the text in settings. There is no per trainee language for push.

Hebrew in code

Several modules contain Hebrew string literals on purpose:
  • modules/form-sign/: every error the signing page shows.
  • modules/assistant/ and modules/agent/: system prompts, canned replies, cap and error messages.
  • modules/plan-import/ocr.ts and the structurers: prompts.
  • ai/nutrition-analyzer.ts: the prompt is English but instructs the model to answer with Hebrew names and notes.
  • modules/trainee/trainee.service.ts: the display name for a deleted trainee.
  • modules/foods/food-enums.ts: household measure and unit labels.
  • Sorting by name uses localeCompare(other, 'he') in a few list builders.
These are product decisions for a Hebrew first market, not missing translations. Do not route them through @repo/i18n without a reason, because that package is consumed from dist and the strings would be far from the code that depends on their exact wording (tests pin some of them).

Catalog data

Exercise names, instructions and food names are data. The Hebrew exercise catalog and instructions are imported from files under apps/core-api/src/scripts/data/ by the import scripts. Localized display of that data is handled by the clients.

Rules for new server text

  • An error on /v1: write a short English message for developers and logs. If a client must react to it, add a stable details.reason. The client translates.
  • An error a coach reads directly (automation API, MCP): write a full English sentence that says what to fix.
  • An email: add a template and keys in mail.json for all six locales. See Mail.
  • A push: add a notification type with default text and let studios edit it. See Notifications and push.
  • Never build a user facing sentence by concatenating translated fragments. Hebrew and Arabic word order and direction break it.
  • Direction: any server rendered document (email, PDF) must set direction from getLocaleDirection. The signed PDF renderer has its own bidirectional text handling in modules/form-sign/visual-text.ts.