Casa de Dios is my church. It's in Houston, and the congregation speaks Spanish and English, sometimes in the same conversation. When I built its website, a bilingual site was the requirement from the first day. What "bilingual" meant changed halfway through.
The version I built first
I started with the standard approach. Each language gets its own URLs, with a [locale] segment in the route and a library to load the right strings. There is still a branch in the repo with a commit called "implement next-intl URL-based i18n routing".
For content that comes from the CMS, I wrote a small helper that picks a field by language:
export function getLocalizedField(doc: object, field: string, locale: string): string {
const record = doc as Record<string, unknown>;
if (locale === 'es') {
const esField = `${field}Es`;
if (esField in record && record[esField]) return record[esField] as string;
}
return (record[field] ?? '') as string;
}Ask for title in Spanish and you get titleEs, falling back to English if the Spanish is empty. A visitor picks a language and sees only that one. It is the standard pattern and it worked.
What the church said
When I showed our church admin an early build, they pushed back. We operate in both languages at once, and they wanted the site to do the same.
They wanted English and Spanish visible together, on every page.
What shipped
Every page prints both. English first, Spanish directly under it:
<h3>{announcement.title}</h3>
<h4>{announcement.titleEs}</h4>
<p>{announcement.message}</p>
<p>{announcement.messageEs}</p>There is one URL per page, with no locale segment and no translation library. The branch with the routing was never merged.
The part that made this easy was a decision in the content model, not in the pages. In the CMS, every text field has a partner (trimmed here):
defineField({ name: 'title', title: 'Title (English)', validation: (Rule) => Rule.required() }),
defineField({ name: 'titleEs', title: 'Title (Spanish)', validation: (Rule) => Rule.required() }),One document holds both languages, and the editor fills in one form. Nobody has to keep two copies of an announcement in step, and because both titles are required, an announcement can't be published with only one.
The header does have a language toggle. It swaps which language leads in the navigation and in the gallery's filters, and remembers the choice. It does not hide anything. The page content always shows both.
getLocalizedField is still in the codebase. Nothing calls it.
What this version gets wrong
Showing both languages has costs, and I haven't paid all of them.
The page is declared as English, with lang="en" on the document, and the Spanish text isn't marked as Spanish. A screen reader will likely read those lines with English pronunciation. Each Spanish element should carry lang="es". That is a small fix and it is not done.
Pages are also roughly twice as long as they would be in one language. On this site that is fine, because the content is short: service times, events, announcements. On a site with long articles it would not be.
And a search engine sees one page in two languages, not a Spanish page and an English page. For a church that people find by name and by neighborhood, I think that is the right trade. For a site that needs to rank for Spanish queries on their own, separate URLs would be the better call.
What I'd take from it
I assumed "bilingual" meant what the tooling assumes it means. It's my own church, and I still built the version that makes people choose. The admin meant something else, and their version was less code.
It would have been cheaper to find that out before building the routing. The question I'd ask first next time is who reads the site and whether they read it alone or together.
The full write-up is in the case study.