Building Multilingual Yacht Websites for Mediterranean Brokerages
Last year we built a yacht brokerage platform that needed to serve listings in six languages across the Mediterranean. French buyers in Cannes, Italian clients in Porto Cervo, Greek charter seekers in Athens, Turkish marina operators in Bodrum, Spanish brokers in Palma de Mallorca, and English-speaking expats scattered everywhere in between. What started as "just add a translate button" turned into one of the most interesting i18n challenges I've worked on. Here's everything I learned about building multilingual yacht websites that actually convert.
Table of Contents
- Why Mediterranean Yacht Brokerages Need Multilingual Sites
- Choosing the Right Tech Stack
- URL Strategy for Multilingual Yacht Listings
- Translating Yacht Listing Data
- i18n Implementation Patterns
- Handling Currency and Unit Localization
- SEO for Multilingual Yacht Websites
- Performance Considerations
- Real-World Architecture Example
- FAQ

Why Mediterranean Yacht Brokerages Need Multilingual Sites
The Mediterranean yacht market is projected to reach $12.3 billion by 2026, according to Allied Market Research. But here's the thing most agencies miss: this market is fundamentally fragmented by language. A 45-meter Benetti listed in Monaco needs to be discoverable by a German industrialist searching in German, a Saudi buyer browsing in Arabic, and a British retiree looking in English.
I've seen brokerages lose six-figure commissions because their site only served English content. A broker in Antibes told me his biggest competitor was winning French-speaking clients simply because their listings showed up in French Google results. That's not a technology problem — it's a business problem with a technology solution.
The numbers back this up:
| Market Segment | Languages Needed | Buyer Demographics |
|---|---|---|
| French Riviera | FR, EN, RU, AR | European HNWIs, Middle Eastern buyers |
| Italian Coast | IT, EN, DE, FR | Northern European charter clients |
| Greek Islands | EL, EN, DE, FR | Charter-heavy, seasonal tourism |
| Turkish Riviera | TR, EN, DE, RU | Budget-conscious charter market |
| Balearic Islands | ES, EN, DE, FR | Mixed brokerage and charter |
| Croatian Coast | HR, EN, DE, IT | Emerging market, growing fast |
If you're only serving one or two of these languages, you're leaving money on the table. Period.
Choosing the Right Tech Stack
For yacht brokerage sites, you need a stack that handles two very different content types: static marketing content (about pages, service descriptions, team bios) and dynamic listing data (yacht specs, pricing, availability, photos).
I've built these on both Next.js and Astro, and both work well depending on your requirements. If you need heavy interactivity — saved searches, comparison tools, inquiry forms with real-time availability — Next.js is the better fit. If the site is primarily a showcase with less dynamic behavior, Astro's island architecture gives you incredible performance out of the box.
Here's how the stacks compare for this specific use case:
| Feature | Next.js (App Router) | Astro | Remix |
|---|---|---|---|
| i18n routing | Built-in middleware | Manual or plugin | Manual |
| Static generation | Excellent | Excellent | Limited |
| Dynamic listings | Native SSR/ISR | On-demand with endpoints | Native SSR |
| CMS integration | Excellent | Excellent | Good |
| Edge rendering | Vercel Edge, Cloudflare | Cloudflare, Netlify | Cloudflare |
| Translation libraries | next-intl, next-i18next | astro-i18n, paraglide | remix-i18next |
| Build time (500 listings × 6 langs) | ~4 min with ISR | ~8 min full static | N/A (SSR) |
For the headless CMS layer, I strongly recommend separating your listing data from your marketing content. Use a purpose-built yacht management system (like Yatco API, NauticEd, or a custom Supabase backend) for listing data, and a headless CMS like Sanity or Contentful for everything else.
Why Headless Matters Here
Yacht data is weird. You've got specs in meters or feet (depending on the audience), prices in euros or dollars, engine hours that update constantly, and availability calendars that change daily. Trying to manage all of that inside a traditional CMS is a nightmare. A headless approach lets you pull listing data from a specialized API and marketing content from a CMS, then combine them at build time or request time.
URL Strategy for Multilingual Yacht Listings
This is where most projects go wrong early. Your URL structure for a multilingual site is one of the hardest decisions to reverse later. There are three approaches:
Subdirectory Pattern (Recommended)
https://yachtbroker.com/en/yachts/benetti-45m-2022
https://yachtbroker.com/fr/yachts/benetti-45m-2022
https://yachtbroker.com/de/yachten/benetti-45m-2022
This is what I recommend for 90% of yacht brokerages. Single domain, shared domain authority, easy to implement with Next.js middleware or Astro's built-in i18n routing.
Subdomain Pattern
https://en.yachtbroker.com/yachts/benetti-45m-2022
https://fr.yachtbroker.com/yachts/benetti-45m-2022
Some larger brokerages prefer this for organizational reasons. Each subdomain can be deployed independently. But you lose consolidated domain authority, and it's more infrastructure to manage.
ccTLD Pattern
https://yachtbroker.fr/yachts/benetti-45m-2022
https://yachtbroker.de/yachten/benetti-45m-2022
Only makes sense if you have separate legal entities in each country. Expensive, complex, and rarely worth it unless you're a Burgess or Fraser-level operation.
Slug Translation
Here's a detail that trips people up: should you translate the URL slugs? For yacht names, no — keep them consistent. A "Benetti Oasis 40M" is called that in every language. But category paths? Yes, translate those.
// next.config.js - Next.js i18n routing
const nextConfig = {
i18n: {
locales: ['en', 'fr', 'de', 'it', 'es', 'el'],
defaultLocale: 'en',
localeDetection: true,
},
};
For translated paths in Next.js App Router with next-intl:
// src/navigation.ts
import { createLocalizedPathnameNavigation } from 'next-intl/navigation';
export const localePrefix = 'always';
export const pathnames = {
'/yachts': {
en: '/yachts',
fr: '/yachts',
de: '/yachten',
it: '/yacht',
es: '/yates',
el: '/skafi',
},
'/yachts/[slug]': {
en: '/yachts/[slug]',
fr: '/yachts/[slug]',
de: '/yachten/[slug]',
it: '/yacht/[slug]',
es: '/yates/[slug]',
el: '/skafi/[slug]',
},
};
export const { Link, redirect, usePathname, useRouter } =
createLocalizedPathnameNavigation({ locales, localePrefix, pathnames });

Translating Yacht Listing Data
This is the core challenge. Yacht listings have three types of content that each need different translation approaches:
1. Structured Data (Don't Translate, Localize)
Specs like length, beam, draft, engine power — these don't need translation. They need localization. Show meters to Europeans, feet to Americans. Show kilowatts to some markets, horsepower to others.
// utils/localize-specs.ts
const UNIT_PREFERENCES: Record<string, UnitSystem> = {
en: 'imperial',
'en-GB': 'metric', // British market uses meters for yachts
fr: 'metric',
de: 'metric',
it: 'metric',
es: 'metric',
el: 'metric',
};
export function localizeLength(meters: number, locale: string): string {
const system = UNIT_PREFERENCES[locale] || 'metric';
if (system === 'imperial') {
const feet = meters * 3.28084;
return `${feet.toFixed(0)} ft`;
}
return `${meters.toFixed(1)} m`;
}
2. Enumerated Fields (Use Translation Keys)
Hull type, fuel type, yacht category — these are fixed options that should use translation keys, not free-text translation.
// messages/en.json
{
"yacht": {
"hullType": {
"monohull": "Monohull",
"catamaran": "Catamaran",
"trimaran": "Trimaran"
},
"fuelType": {
"diesel": "Diesel",
"electric": "Electric",
"hybrid": "Hybrid"
}
}
}
// messages/fr.json
{
"yacht": {
"hullType": {
"monohull": "Monocoque",
"catamaran": "Catamaran",
"trimaran": "Trimaran"
},
"fuelType": {
"diesel": "Diesel",
"electric": "Électrique",
"hybrid": "Hybride"
}
}
}
3. Free-Text Descriptions (The Hard Part)
Yacht descriptions are marketing copy. They're written by brokers — usually in English or French — and they're full of industry jargon, emotional language, and specific claims. Machine translation alone won't cut it for a €5 million listing.
Here's the approach I recommend:
- Store the original language description in your CMS/database
- Use AI translation as a first pass — GPT-4o or Claude handle yacht terminology surprisingly well in 2025
- Flag listings above a price threshold (say, €1M+) for human review
- Cache translated descriptions so you're not paying for re-translation on every request
// services/translate-listing.ts
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
export async function translateDescription(
text: string,
sourceLang: string,
targetLang: string
): Promise<string> {
const cached = await getFromCache(text, targetLang);
if (cached) return cached;
const { text: translated } = await generateText({
model: openai('gpt-4o'),
system: `You are a professional yacht broker translator.
Translate yacht listing descriptions from ${sourceLang} to ${targetLang}.
Preserve technical terminology. Maintain the luxury marketing tone.
Keep brand names, model names, and proper nouns unchanged.`,
prompt: text,
});
await saveToCache(text, targetLang, translated);
return translated;
}
The cost of this approach is minimal. Translating a 500-word yacht description with GPT-4o costs roughly $0.01-0.02. Even with 500 listings × 6 languages, you're looking at $30-60 for the initial translation pass. Human review of premium listings adds cost, but it's absolutely worth it when a single yacht sale generates $50K-200K in commission.
i18n Implementation Patterns
Let me walk through the actual implementation pattern I use with Next.js App Router and next-intl, since that's the stack most of our headless CMS projects use.
Project Structure
src/
├── app/
│ └── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ └── yachts/
│ ├── page.tsx
│ └── [slug]/
│ └── page.tsx
├── messages/
│ ├── en.json
│ ├── fr.json
│ ├── de.json
│ ├── it.json
│ ├── es.json
│ └── el.json
├── middleware.ts
└── i18n.ts
Middleware for Locale Detection
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales, localePrefix, pathnames } from './navigation';
export default createMiddleware({
locales,
localePrefix,
pathnames,
defaultLocale: 'en',
localeDetection: true,
});
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};
Yacht Listing Page with Translations
// app/[locale]/yachts/[slug]/page.tsx
import { useTranslations } from 'next-intl';
import { getYachtBySlug } from '@/lib/yachts';
import { localizeLength, localizePrice } from '@/utils/localize';
export async function generateMetadata({ params: { locale, slug } }) {
const yacht = await getYachtBySlug(slug);
const t = await getTranslations({ locale, namespace: 'yacht' });
return {
title: `${yacht.name} — ${localizeLength(yacht.lengthMeters, locale)} ${t('forSale')}`,
alternates: {
languages: {
'en': `/en/yachts/${slug}`,
'fr': `/fr/yachts/${slug}`,
'de': `/de/yachten/${slug}`,
'it': `/it/yacht/${slug}`,
'es': `/es/yates/${slug}`,
'el': `/el/skafi/${slug}`,
},
},
};
}
export default async function YachtPage({ params: { locale, slug } }) {
const yacht = await getYachtBySlug(slug);
const t = useTranslations('yacht');
const description = await getTranslatedDescription(yacht.id, locale);
return (
<article>
<h1>{yacht.name}</h1>
<dl>
<dt>{t('specs.length')}</dt>
<dd>{localizeLength(yacht.lengthMeters, locale)}</dd>
<dt>{t('specs.year')}</dt>
<dd>{yacht.year}</dd>
<dt>{t('specs.price')}</dt>
<dd>{localizePrice(yacht.priceEur, locale)}</dd>
<dt>{t('specs.hullType')}</dt>
<dd>{t(`hullType.${yacht.hullType}`)}</dd>
</dl>
<div dangerouslySetInnerHTML={{ __html: description }} />
</article>
);
}
Handling Currency and Unit Localization
Yacht pricing in the Mediterranean is almost always listed in euros, but buyers from different markets expect to see reference prices in their local currency. Here's how I handle it:
// utils/localize-price.ts
const CURRENCY_BY_LOCALE: Record<string, string> = {
en: 'EUR', // International English defaults to EUR in Med market
'en-US': 'USD',
fr: 'EUR',
de: 'EUR',
it: 'EUR',
es: 'EUR',
el: 'EUR',
tr: 'EUR', // Turkish market still prices in EUR
ar: 'USD', // Middle Eastern buyers prefer USD
ru: 'EUR',
};
export function localizePrice(
priceEur: number,
locale: string,
exchangeRates?: Record<string, number>
): string {
const currency = CURRENCY_BY_LOCALE[locale] || 'EUR';
let amount = priceEur;
if (currency !== 'EUR' && exchangeRates) {
amount = priceEur * (exchangeRates[currency] || 1);
}
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
maximumFractionDigits: 0,
}).format(amount);
}
Important caveat: always show "Price in EUR" or equivalent disclaimer when displaying converted prices. Yacht sales contracts are denominated in a specific currency, and showing a converted price without context can create legal issues.
SEO for Multilingual Yacht Websites
This is where the real payoff happens. Proper multilingual SEO means your Azimut 68 listing shows up when someone in Munich searches "Azimut 68 kaufen" AND when someone in Paris searches "Azimut 68 à vendre."
hreflang Tags
These are non-negotiable. Every page needs hreflang tags pointing to all language versions:
<link rel="alternate" hreflang="en" href="https://broker.com/en/yachts/azimut-68-2023" />
<link rel="alternate" hreflang="fr" href="https://broker.com/fr/yachts/azimut-68-2023" />
<link rel="alternate" hreflang="de" href="https://broker.com/de/yachten/azimut-68-2023" />
<link rel="alternate" hreflang="x-default" href="https://broker.com/en/yachts/azimut-68-2023" />
Structured Data Per Language
Use Product schema with localized descriptions for each language version. Google explicitly supports language-specific structured data, and it helps your listings appear in rich results across different Google domains.
Sitemap Strategy
Generate separate sitemaps per language and reference them from a sitemap index:
<!-- sitemap-index.xml -->
<sitemapindex>
<sitemap><loc>https://broker.com/sitemap-en.xml</loc></sitemap>
<sitemap><loc>https://broker.com/sitemap-fr.xml</loc></sitemap>
<sitemap><loc>https://broker.com/sitemap-de.xml</loc></sitemap>
</sitemapindex>
Performance Considerations
A yacht listing page with 30+ high-res photos, translated content, and localized specs can get heavy fast. Here's what matters:
- ISR (Incremental Static Regeneration): Regenerate listing pages every 60 minutes. Yacht listings don't change by the second, but pricing and availability can shift daily.
- Translation caching: Never translate the same description twice. Use Redis or even a simple database table to cache translations.
- Image optimization: This is often the biggest win. A single yacht gallery can contain 2GB of source images. Use Next.js Image or a CDN with automatic format negotiation (WebP/AVIF).
- Bundle splitting by locale: Don't load French translations for English users. Both
next-intlandparaglidehandle this automatically.
On a recent project, these optimizations brought our Largest Contentful Paint from 4.2s to 1.1s across all locales. That matters when your bounce rate directly correlates with lost commissions.
Real-World Architecture Example
Here's the architecture we've used for Mediterranean brokerage sites:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Sanity │ │ Yacht API │ │ Redis │
│ (Marketing │ │ (Listings, │ │ (Translation│
│ content) │ │ specs) │ │ cache) │
└──────┬───────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
└────────────┬───────┘────────────────────┘
│
┌───────▼───────┐
│ Next.js │
│ App Router │
│ + next-intl │
└───────┬───────┘
│
┌───────▼───────┐
│ Vercel │
│ Edge Network│
└───────────────┘
Sanity handles the marketing pages, team bios, blog posts — all of which have native multi-language support. The yacht API (either a third-party service or custom Supabase backend) provides listing data. Redis caches AI-generated translations. Next.js ties it all together with locale-aware routing.
If this kind of architecture sounds like what you need, we'd love to talk about your project. We've built several of these for Mediterranean brokerages and have the patterns dialed in.
FAQ
How many languages should a Mediterranean yacht website support? At minimum, you need English plus the local language of your primary market. For serious brokerages, I recommend English, French, German, and Italian as a baseline — that covers roughly 80% of Mediterranean yacht buyers. Add Russian and Arabic if you're targeting the ultra-luxury segment above €5M.
Should I use machine translation or hire professional translators for yacht listings? Both. Use AI translation (GPT-4o or Claude) as a first pass for all listings, then have human translators review listings above your price threshold. For a 500-word description, AI translation costs under $0.02 and gets you 90% of the way there. Human review for premium listings costs $20-50 per description but ensures accuracy for high-value sales.
What's the best CMS for multilingual yacht websites? Sanity and Contentful both handle multi-language content well out of the box. Sanity's document-level localization gives you more flexibility, while Contentful's field-level localization is simpler to set up. For the yacht listing data itself, I recommend a separate specialized system rather than trying to force everything into a general-purpose CMS. Check our headless CMS development page for more details.
How do I handle yacht measurements in different unit systems?
Store all measurements in metric (meters, kilowatts) in your database. Convert to imperial only at the display layer based on the user's locale. The yacht industry in Europe universally uses metric, but American buyers expect feet and horsepower. Use the Intl.NumberFormat API for consistent formatting.
Do hreflang tags really matter for yacht SEO? Absolutely. Without hreflang tags, Google might show your French listing to German searchers, or worse, treat your translated pages as duplicate content. We've seen organic traffic increase 40-60% after properly implementing hreflang across a brokerage site that previously had it wrong.
How much does it cost to build a multilingual yacht brokerage website? A properly built multilingual yacht site with 4-6 languages, CMS integration, and yacht listing management typically runs $30,000-80,000 depending on complexity. The biggest cost drivers are the number of languages, custom search/filter functionality, and integration with existing yacht management systems. Visit our pricing page for more specific estimates.
Can I add languages to my yacht website later? Yes, if it's built correctly from the start. With a proper i18n architecture, adding a new language means creating a new translation file, translating your static UI strings, and running your listing descriptions through the translation pipeline. The routing and infrastructure should already handle it. If your current site wasn't built with i18n in mind, a retrofit is harder — but still doable.
What about right-to-left languages like Arabic for yacht websites?
Arabic is increasingly important for Mediterranean yacht sales, especially in the €10M+ segment. Your CSS needs to support RTL layouts — use logical properties (margin-inline-start instead of margin-left) and test thoroughly. Next.js supports RTL with the dir attribute on your HTML element, switched per locale. It adds development time, but Middle Eastern buyers represent a significant and growing market segment.