Multilingual labels β
GCM ships with English, Spanish, French, and Portuguese as supported UI languages. The UI strings (button labels, menu items, error messages) come from the i18n/locales/<lang>.json translation files maintained by the platform. But the data β your member types, your meeting names, your countries, your relationship statuses β lives in the database. To make those labels follow the user's chosen language, every reference table has a translations JSONB column.
This article covers what's in that column, when you should fill it out, and how the rendering falls back when a translation is missing.

The translations shape β
Every reference-table row has a JSONB column called translations with this shape:
{
"en": { "name": "Visitor" },
"es": { "name": "Visitante" },
"fr": { "name": "Visiteur" },
"pt": { "name": "Visitante" }
}The top-level keys are locale codes (en, es, fr, pt). The value of each is an object of field name β translated string. Most reference rows only have a name field today, but the same column extends cleanly to other localizable fields (description, plural form) without a migration.
Locale codes match the SUPPORTED_LOCALES constant in src/shared/lib/i18n-localize.ts:
| Code | Language |
|---|---|
en | English |
es | EspaΓ±ol |
fr | FranΓ§ais |
pt | PortuguΓͺs |
How the resolution works β
When the UI needs to render a row's name, it calls the localize() helper with the row's translations, the legacy name column as fallback, and the user's active locale:
localize(row.translations, row.name, language, "name")The helper walks four resolution steps in order:
translations[locale][field]β exact hit on the requested locale.translations.en[field]β English fallback.row[field]β the legacy canonical column. (For member types, that'smember_types.name.)- Empty string β caller can render a placeholder.
So a Spanish user viewing a member type with no Spanish translation sees the English label rather than a blank cell. A user viewing a meeting in Portuguese β a less-translated locale β sees Portuguese if available, then English, then the raw name column. No locale silently shows an empty cell.
When you should fill out translations β
The UX cost of leaving translations blank is small (fallback to English) but visible to your bilingual congregants. Three rules of thumb:
Always translate if your congregation is bilingual β
If you regularly preach in two languages or have members who only read one, fill in both labels every time you add a reference row. The 20 seconds it takes when you add a member type saves an awkward English-only label in a Spanish member's profile.
Translate the platform-managed lists too β
Countries, states, cities, genders, relationship statuses β these come pre-translated by the platform but the coverage isn't 100%. If you notice a country or state showing English in your Spanish UI, you can flag it via support and the platform admin will fill in the missing locale.
Skip translations if you're monolingual β
If your whole congregation speaks one language and you have no plans to add another, skip the translations input. The fallback chain ensures the canonical name renders correctly. You can always come back and fill them in later β adding a translation never moves the row, it just adds to the JSONB.
The translations input UI β
When you add or edit a reference row, the dialog shows a Translations input.
It looks like a tabbed text input β one tab per supported locale, defaulting to English. Type the English label first (which mirrors into the canonical name column), then switch to Spanish / French / Portuguese tabs to fill in the rest.
The tabs you see depend on SUPPORTED_LOCALES. Adding a new platform language is a one-line change in i18n-localize.ts plus shipping the matching UI translations file β no database migration. Existing rows just have empty entries for the new locale until you fill them in.
Bulk translation
For org admins setting up a brand-new Spanish-language tenant, the fastest path is:
- Add every member type in English first.
- Open the database via the Data Logs module or your Supabase project.
- Use a single UPDATE:
UPDATE member_types SET translations = jsonb_set(translations, '{es,name}', '"<spanish name>"') WHERE name = '<english name>'. - Refresh the UI β every member already assigned to that type now shows the Spanish label.
Platform admins can do this for the global reference tables (genders, statuses, geography) on request.
What gets translated where β
| Table | Field | Who fills it in |
|---|---|---|
member_types | name | Org admin |
meetings | name | Org admin |
genders | name | Platform admin |
relationship_statuses | name | Platform admin |
countries | name | Platform admin |
states | name | Platform admin |
cities | name | Platform admin |
pages (website builder) | title, body | Org admin (separate UI) |
notifications | subject, body | Org admin (separate UI) |
events | title, description | Org admin (event editor) |
The pattern is consistent across the platform: anywhere user-entered data needs to follow the viewer's language, the row gets a translations JSONB column and the UI reads it through the same localize() helper.
Migration history β
The translations column was added platform-wide in migration 20260518020000_translations_jsonb_system along with the localize() helper. Before that, individual tables had _es columns (e.g. meetings.name_es) which were inconsistent and didn't extend to new languages. Those _es columns have been migrated out across meetings, months, events, pages, and notifications β see the Codebase docs for the canonical reference.
If you find an old _es column in src/integrations/supabase/types.ts, it's a leftover in the auto-generated type file rather than something you should write to. Always write through the translations JSONB column.
What if I need a fifth language? β
Add the locale code to SUPPORTED_LOCALES in src/shared/lib/i18n-localize.ts, ship a matching src/i18n/locales/<code>.json file with the UI strings, and the database is ready to go without a migration. Existing rows get an empty entry for the new locale; admins fill them in over time. Email the engineering team if your tenant needs a language not in the supported list.
Next β
- Customize member types β practice the workflow on a list you fully own.
- Genders and relationship statuses β where translations are managed by the platform.
- Countries, states, cities β pre-translated to varying coverage.
