CatalyzeUpDocs
impact pulse / product

Multi-Language Support (i18n)

Multi-Language Support

Why This Matters

Nonprofits operate globally. A single organization might run programs across Haiti (French/Creole), Guatemala (Spanish), and Somalia (Somali). Beneficiaries taking surveys must see their native language. Admins create surveys in English (or their language), and the platform handles translation automatically.

This is not a nice-to-have. For ImpactPulse to serve global nonprofits, multi-language is a core requirement.

Design Principles

  1. English keys, translated display. The backend stores data in the original language (default English). The frontend renders translations. Scoring never touches translations.
  2. Auto-translate via AI agent. Adding a new language should be one click. An AI agent translates all content and a human reviewer approves.
  3. Survey-level translations, not platform-level. The platform UI (buttons, navigation, labels) is a separate translation layer from survey content (questions, options).
  4. Translations are additive. You can always add a language later without changing existing data or breaking scores.

Two Translation Layers

Layer 1: Platform UI (Static)

All buttons, labels, navigation, error messages, and system text.

/frontend/src/locales/
  en.json    # English (default, source of truth)
  fr.json    # French
  es.json    # Spanish
  ar.json    # Arabic (RTL)
  sw.json    # Swahili
  ...

Example en.json:

{
  "nav.dashboard": "Dashboard",
  "nav.organizations": "Organizations",
  "nav.signOut": "Sign Out",
  "survey.startSurvey": "Start Survey",
  "survey.next": "Next",
  "survey.previous": "Previous",
  "survey.submit": "Submit",
  "survey.yes": "Yes",
  "survey.no": "No",
  "survey.ratingLabels": ["N/A", "Poor", "Fair", "Good", "Very Good", "Excellent"],
  "auth.signIn": "Sign in",
  "auth.createAccount": "Create account",
  "auth.email": "Email address",
  "auth.password": "Password"
}

Implementation: Use react-i18next or a lightweight alternative. Detect browser locale, allow manual override. Store preference in user profile.

Adding a new language: An AI agent reads en.json, translates every key, outputs {lang}.json. A human reviews and merges.

Layer 2: Survey Content (Dynamic)

Questions, options, descriptions, and survey titles. This is per-survey, created by admins.

Data Model

questions collection (additional fields):
  text: "Are you happy?"                    # Original (English)
  translations: {
    "fr": "Êtes-vous heureux?",
    "es": "¿Estás feliz?",
    "sw": "Je, una furaha?"
  }
  options: ["Family", "Friends", "Community"]   # English keys (used for scoring)
  option_translations: {
    "fr": {"Family": "Famille", "Friends": "Amis", "Community": "Communauté"},
    "es": {"Family": "Familia", "Friends": "Amigos", "Community": "Comunidad"}
  }

surveys collection (additional fields):
  title: "Community Impact Assessment"
  title_translations: {
    "fr": "Évaluation de l'impact communautaire",
    "es": "Evaluación del impacto comunitario"
  }
  description: "Assess the impact of our programs"
  description_translations: {
    "fr": "Évaluez l'impact de nos programmes",
    "es": "Evalúe el impacto de nuestros programas"
  }
  available_languages: ["en", "fr", "es"]    # Languages this survey supports
  default_language: "en"

How Scoring Stays Language-Neutral

The response always stores the English key, regardless of what language the user saw:

{
  "responses": {
    "q_bool": true,           // Not "Oui" or "Si", always true/false
    "q_rating": 4,            // Always a number
    "q_mc": "Family"          // Always the English option key
  }
}

The scoring engine never changes. It receives true/false, numbers, and English option keys. Translations are purely a frontend rendering concern.

Frontend Rendering Logic

function getQuestionText(question: Question, locale: string): string {
  if (locale === 'en' || !question.translations?.[locale]) {
    return question.text  // Fallback to English
  }
  return question.translations[locale]
}

function getOptionLabel(question: Question, option: string, locale: string): string {
  if (locale === 'en' || !question.option_translations?.[locale]?.[option]) {
    return option  // Fallback to English key
  }
  return question.option_translations[locale][option]
}

The submitted value is always the English key:

// User sees "Famille" but the value sent is "Family"
onChange={() => setResponse(question.id, option)}  // option = "Family"

AI Translation Agent

Workflow

  1. Admin creates a survey in English
  2. Admin clicks "Add Language" and selects French
  3. The AI agent receives:
    • All question texts
    • All option labels
    • Survey title and description
    • Target language
  4. Agent translates everything in one batch
  5. Translations are saved as a draft
  6. Admin (or a bilingual team member) reviews and approves
  7. Survey is now available in French

Agent Implementation

POST /api/v1/surveys/{id}/translate
{
  "target_language": "fr",
  "auto_approve": false    // true for trusted languages
}

The agent uses Claude API (or similar) with a system prompt:

You are translating a nonprofit impact measurement survey.
Context: This survey is taken by beneficiaries of social programs.
The language must be simple, clear, and culturally appropriate.
Do not use formal/academic language. Use everyday words.
Translate for comprehension, not for literary quality.

Source language: English
Target language: French

Translate each item. Return JSON with the same keys.

Batch Translation for New Languages

When adding a language across the entire platform:

  1. Translate all en.json platform strings (one-time, ~200 keys)
  2. For each survey that opts in: translate questions + options
  3. Store as draft translations, flag for review
  4. Reviewer approves per-survey

Quality Control

  • Translations are always draft until approved
  • Side-by-side review UI: English on left, translation on right
  • Flag individual translations for re-review
  • Track which translations were AI-generated vs human-written
  • Version history on translations (same as survey versions)

Language Selection UX

Survey Taker

When a respondent opens a survey link:

  1. Detect browser language
  2. If the survey supports that language, use it
  3. If not, show a language picker at the top of the survey
  4. Language choice is persisted for the session
  5. The URL can include ?lang=fr to force a language

Admin

  • Survey editor shows a "Languages" tab
  • Lists available translations with status (draft, approved, needs review)
  • "Add Language" button triggers AI translation
  • "Edit Translation" opens side-by-side editor
  • "Remove Language" removes translation (does not affect responses)

Organization Settings

  • Default language for the organization
  • Preferred languages list (pre-populate when creating new surveys)
  • Auto-translate new surveys into preferred languages

RTL (Right-to-Left) Support

For Arabic, Hebrew, Farsi, Urdu:

  • HTML dir="rtl" attribute on the survey container
  • Tailwind: use rtl: variant for directional styles
  • Mirror layout: back button on right, next on left
  • Number inputs remain LTR (universal convention)
  • Progress bar fills right-to-left

Supported Languages (Initial Target)

Based on top nonprofit operating regions:

Language Code Script Direction
English en Latin LTR
French fr Latin LTR
Spanish es Latin LTR
Portuguese pt Latin LTR
Arabic ar Arabic RTL
Swahili sw Latin LTR
Hindi hi Devanagari LTR
Haitian Creole ht Latin LTR
Amharic am Ge'ez LTR
Somali so Latin LTR
Burmese my Myanmar LTR
Dari/Farsi fa Arabic RTL

More languages added on demand via the AI translation agent.

Implementation Phases

Phase A: Foundation

  • Install react-i18next and configure with en.json
  • Extract all hardcoded strings from frontend to locale keys
  • Add language selector to user profile
  • Store user language preference in database

Phase B: Survey Content Translation

  • Add translations and option_translations fields to question model
  • Add title_translations, description_translations, available_languages to survey model
  • Update survey take page to render translated content
  • Add "Languages" tab to survey editor
  • Language picker on public survey page

Phase C: AI Translation Agent

  • Build translation endpoint: POST /surveys/{id}/translate
  • Integrate Claude API for translation
  • Side-by-side review UI for admins
  • Draft/approved workflow for translations
  • Batch translation for new languages

Phase D: RTL and Polish

  • Add RTL support for Arabic, Farsi, Hebrew
  • Test all components in RTL mode
  • Add language-specific formatting (date, number formats)
  • Performance: lazy-load language files

Data Migration

No migration needed. Translations are additive fields:

  • translations: {} defaults to empty (English only)
  • option_translations: {} defaults to empty
  • available_languages: ["en"] defaults to English
  • Existing surveys continue to work unchanged

Success Metrics

  • Time to add a new language: under 5 minutes (AI translate + review)
  • Survey completion rate in translated languages vs English
  • Number of languages per organization
  • Translation approval rate (AI quality)