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
- English keys, translated display. The backend stores data in the original language (default English). The frontend renders translations. Scoring never touches translations.
- Auto-translate via AI agent. Adding a new language should be one click. An AI agent translates all content and a human reviewer approves.
- Survey-level translations, not platform-level. The platform UI (buttons, navigation, labels) is a separate translation layer from survey content (questions, options).
- 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
- Admin creates a survey in English
- Admin clicks "Add Language" and selects French
- The AI agent receives:
- All question texts
- All option labels
- Survey title and description
- Target language
- Agent translates everything in one batch
- Translations are saved as a draft
- Admin (or a bilingual team member) reviews and approves
- 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:
- Translate all
en.jsonplatform strings (one-time, ~200 keys) - For each survey that opts in: translate questions + options
- Store as draft translations, flag for review
- 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:
- Detect browser language
- If the survey supports that language, use it
- If not, show a language picker at the top of the survey
- Language choice is persisted for the session
- The URL can include
?lang=frto 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-i18nextand configure withen.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
translationsandoption_translationsfields to question model - Add
title_translations,description_translations,available_languagesto 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 emptyavailable_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)