Translation System
Birdhouse questionnaires support multiple languages through an internationalization (i18n) system. This page explains how translatable content moves from the CMS through the sync pipeline and into the user’s browser in their language.
The Core Idea
Section titled “The Core Idea”Every piece of user-facing text in a questionnaire — titles, descriptions, button labels, option text — needs to be translatable. Rather than storing translations in Strapi (which would make the CMS unwieldy), Birdhouse uses a wrapper approach:
- The sync script wraps all translatable strings with
t()function calls - The generated TypeScript file exports a function, not a static object
- At runtime, the app calls that function with its i18n
TFunction - All
t()calls resolve to the correct translation (or fall back to the default)
How It Works Step by Step
Section titled “How It Works Step by Step”flowchart TD
A["<b>Strapi CMS</b><br/>Stores default text:<br/>'When were you born?'"] --> B["<b>Sync Script</b><br/>Wraps with t():<br/>t('dob.title', 'When were you born?')"]
B --> C["<b>Generated .ts file</b><br/>Exports function:<br/>(tFunction, data) => questionnaire"]
C --> D["<b>App at runtime</b><br/>Calls function with<br/>i18n TFunction"]
D --> E{"Translation<br/>key found?"}
E -->|Yes| F["Shows translated text:<br/>'Wann wurden Sie geboren?'"]
E -->|No| G["Shows default text:<br/>'When were you born?'"]
The Generated File Structure
Section titled “The Generated File Structure”The sync script produces a file like this (simplified):
export const getQuestionnaire = (t: TFunction, interpolationData?: Record<string, unknown>) => ({ questions: [ { questionId: 'dateOfBirth', screen: { title: t('dateOfBirth.title', 'When were you born?'), subtitle: t('dateOfBirth.subtitle', 'We need this to calculate your premium'), }, // ... } ]});The first argument to t() is the translation key — a structured identifier that translation teams use to provide the localized version. The second argument is the default value — the English text from Strapi that serves as a fallback.
At Runtime
Section titled “At Runtime”The app imports this function and calls it with its i18n TFunction:
import { getQuestionnaire } from '@/syncedData/household';import { useTranslation } from 'react-i18next';
const { t } = useTranslation();const questionnaire = getQuestionnaire(t);From this point on, every text field in the questionnaire contains the translated string (or the English default if no translation exists for the user’s language).
Dynamic Text with interpolate()
Section titled “Dynamic Text with interpolate()”Some strings contain dynamic values — things like the user’s name, a price, or a date. These use the interpolate() wrapper:
title: t('greeting.title', 'Hello {{name}}, your premium is {{price}}')The double-brace syntax {{name}} is replaced at runtime with actual values from interpolationData. This is the standard i18next interpolation format.
Constants in Translations
Section titled “Constants in Translations”Questionnaire metadata includes a constants object — key-value pairs defined in Strapi that represent reusable values (like minimum coverage amounts, age thresholds, or product names).
These constants are available in t() options, so translations can reference them:
t('coverage.description', 'Minimum coverage: {{minCoverage}}', { minCoverage: constants.minCoverage })This means Product teams can change a value in one place (the constants in Strapi) and have it reflected everywhere it appears in the questionnaire.
Shared Configuration
Section titled “Shared Configuration”Beyond per-questionnaire constants, there is a sharedConfig object that provides global values available to all questionnaires:
| Constant | Example | Purpose |
|---|---|---|
featherName |
“Feather” | Brand name (may vary by context) |
| Various thresholds | Age limits, income limits | Business rules referenced in text |
These are injected alongside questionnaire-specific constants so that common values do not need to be duplicated across every questionnaire.
Translation Workflow
Section titled “Translation Workflow”flowchart LR
A["Content editor<br/>writes English text<br/>in Strapi"] --> B["Sync script<br/>generates .ts with<br/>t() wrappers"]
B --> C["Translation keys<br/>extracted and sent<br/>to translation team"]
C --> D["Translators add<br/>German/other<br/>language strings"]
D --> E["App loads<br/>translations at<br/>runtime"]
The translation keys generated by the sync script serve as the contract between the codebase and the translation system. As long as keys remain stable, translations continue to work even if the default English text in Strapi changes.
Common Issues
Section titled “Common Issues”| Issue | Cause | Fix |
|---|---|---|
Text shows as a key (e.g., dateOfBirth.title) |
Translation key exists but has no value, and no default was provided | Check that the t() call includes a default value |
| English text shows instead of German | Translation for this key has not been added yet | Add the translation for the key |
Dynamic values show as {{name}} |
interpolationData not passed or missing the key |
Ensure the app passes the correct data when calling the questionnaire function |
Constant shows as {{minCoverage}} |
Constants not included in the t() options | Check that questionnaire constants are being passed through |
See also
Section titled “See also”- Questionnaire Metadata Reference – translationKey and constants fields
- Sync a Questionnaire – how the sync script generates t() wrappers
- Strapi Transform Pipeline – how data flows through the pipeline before i18n wrapping