Skip to content

Translations & Localization

Sell the platform to clinics worldwide — each clinic operates in its own language.

Business Model

RestartiX is a white-label platform sold to clinics internationally. Each clinic (organization) operates in a single language chosen at setup. The platform's internal language is English (code, API field names, database columns), but all user-facing content is in the clinic's language.

This is not a multilingual-clinic model (where one clinic supports multiple languages). It's a multi-market model: a Romanian clinic runs entirely in Romanian, a Spanish clinic runs entirely in Spanish.

What this means in practice

  • Org-created content (services, forms, custom fields, automations, PDF templates) — created directly in the clinic's language. No translation layer needed.
  • Global/platform-curated content (exercises, exercise taxonomy, treatment plan templates) — maintained in English as canonical, with translations provided by the platform team per supported language.
  • Frontend UI (buttons, labels, status names, error messages) — handled by frontend i18n libraries (next-intl), independent of the database. Can be built in parallel later.

Database Architecture

Organization language

Every organization has a language_code that determines the language for all platform-curated content served to that org.

sql
ALTER TABLE organizations ADD COLUMN language_code TEXT NOT NULL DEFAULT 'en';
-- ISO 639-1: 'ro', 'en', 'es', 'fr', 'de', 'pt', etc.

JSONB translations on global content

Global/platform-curated tables get a translations JSONB column. The canonical columns (name, description, etc.) remain in English. Translations are stored as a language-keyed object.

sql
ALTER TABLE exercises ADD COLUMN translations JSONB NOT NULL DEFAULT '{}';

Example data:

jsonc
-- exercises row (global, organization_id IS NULL)
{
  "id": 1,
  "name": "Shoulder External Rotation",           // canonical (English)
  "description": "Strengthens the rotator cuff...",
  "video_url": "https://cdn.example.com/shoulder-rotation-en.mp4",
  "translations": {
    "ro": {
      "name": "Rotație Externă Umăr",
      "description": "Întărește musculatura coifului rotatorilor...",
      "video_url": "https://cdn.example.com/shoulder-rotation-ro.mp4"
    },
    "es": {
      "name": "Rotación Externa de Hombro",
      "description": "Fortalece el manguito rotador...",
      "video_url": "https://cdn.example.com/shoulder-rotation-es.mp4"
    }
  }
}

Query pattern — resolve translation at read time using the org's language, fallback to canonical:

sql
SELECT
  e.id,
  COALESCE(e.translations->$1->>'name', e.name) AS name,
  COALESCE(e.translations->$1->>'description', e.description) AS description,
  COALESCE(e.translations->$1->>'video_url', e.video_url) AS video_url,
  e.difficulty,
  e.estimated_duration_seconds
FROM exercises e
WHERE e.organization_id IS NULL  -- global exercises
ORDER BY e.name;
-- $1 = org.language_code (e.g., 'ro')

Any field not present in the translation object falls back to the canonical English column. This includes non-text fields like video_url — a localized video with spoken instructions in the clinic's language.

Tables requiring translations JSONB

Only global/platform-curated content needs translations. Everything org-scoped is created directly in the org's language.

TableTranslatable fieldsTypical volume
exercisesname, description, video_url, video_thumbnail_url~10,000
exercise_instructionstitle, content, image_url~3–5 per exercise
exercise_contraindicationscondition_name, description~1–3 per exercise
exercise_categoriesname~20–50
exercise_body_regionsname~20–30
exercise_equipmentname~15–25
treatment_plans (scope = 'global')name, description~100–500
treatment_plan_sessions (for global plans)name, description~3–10 per plan

What does NOT need translations in the database

ContentReason
ServicesOrg-scoped — created in org's language
SpecialtiesOrg-scoped — created in org's language
Custom fields (labels, options)Org-scoped — created in org's language
Form templatesOrg-scoped — created in org's language
AutomationsOrg-scoped — created in org's language
PDF templatesOrg-scoped — created in org's language
AppointmentsRuntime data — inherits from service/specialist names
Patient profilesPortable profile fields (name, DOB) are language-neutral
Status enums (booked, completed)Code values — display labels handled by frontend i18n

API Behavior

The Core API resolves translations transparently. When an org queries global content, the API uses the org's language_code to extract the right translation from the JSONB column.

GET /v1/exercises
Authorization: Bearer <token>
X-Organization-Id: 42  (language_code = 'ro')

Response:
{
  "data": [
    {
      "id": 1,
      "name": "Rotație Externă Umăr",
      "description": "Întărește musculatura...",
      "video_url": "https://cdn.example.com/shoulder-rotation-ro.mp4",
      "difficulty": "moderate",
      "is_global": true
    },
    {
      "id": 502,
      "name": "Exercițiu Custom Clinică",
      "description": "...",
      "is_global": false
    }
  ]
}
  • Global exercises (is_global: true) — name/description resolved from translations JSONB
  • Org exercises (is_global: false) — returned as-is, already in org's language
  • No Accept-Language header needed — the org's language is the source of truth

Cloning behavior

When a clinic clones a global exercise to customize it, the clone is created with the translated text baked into the canonical columns:

sql
INSERT INTO exercises (organization_id, name, description, video_url, cloned_from_id, ...)
SELECT
  $org_id,
  COALESCE(e.translations->$lang->>'name', e.name),
  COALESCE(e.translations->$lang->>'description', e.description),
  COALESCE(e.translations->$lang->>'video_url', e.video_url),
  e.id,
  ...
FROM exercises e WHERE e.id = $exercise_id;

The clone is now org-scoped, in the org's language, fully independent. No ongoing translation dependency.


Translation Management

Who translates

  • Platform team — provides translations for all supported languages when entering a new market
  • Clinics — can override by cloning and editing (but don't manage translations directly)

Platform admin workflow

  1. Platform team creates/updates global exercises in English (canonical columns)
  2. Platform team adds translations to the translations JSONB for each supported language
  3. New clinics immediately see content in their language
  4. Existing clinics see updates to global content automatically (unless they cloned and customized)

Adding a new language

  1. Add language to supported languages list
  2. Translate all global content (exercises, taxonomy, treatment plan templates)
  3. Update translations JSONB on affected rows
  4. New orgs can now be created with that language_code

Checking translation coverage

sql
-- Find global exercises missing Romanian translations
SELECT id, name
FROM exercises
WHERE organization_id IS NULL
  AND (translations->'ro'->>'name') IS NULL;

Frontend Localization (Future)

Frontend i18n is independent of the database translation layer and can be built in parallel. This section will be expanded when frontend localization work begins.

Planned approach

  • Library: next-intl for both Clinic app and Patient Portal
  • Routing: app/[locale]/... segment-based routing
  • Message files: JSON per language (messages/en.json, messages/ro.json)
  • Scope: UI chrome (buttons, labels, navigation, status display names, error messages, date/time/number formatting)
  • Locale source: Derived from organization.language_code — not user-selectable

What frontend i18n covers

  • Static UI strings (button text, menu items, page titles, empty states)
  • Status enum display names (booked → "Programat" in Romanian)
  • Date/time formatting (24h vs 12h, DD/MM/YYYY vs MM/DD/YYYY)
  • Number/currency formatting (1.234,56 vs 1,234.56)
  • Relative time ("3 days ago" → "acum 3 zile")
  • Error messages from API (mapped to localized strings on the client)

What frontend i18n does NOT cover

  • Dynamic content (exercise names, service names, form labels) — these come from the API already in the right language
  • Patient-entered data (names, notes, form responses) — language-neutral