去年我们为一个游艇经纪平台开发了一个需要在地中海地区六种语言中提供列表的系统。法国买家在戛纳、意大利客户在波托切尔沃、希腊包租寻求者在雅典、土耳其码头运营商在博德鲁姆、西班牙经纪人在帕尔玛、英语使用者分散在两者之间。最初的"只是添加翻译按钮"演变成了我经历过的最有趣的 i18n 挑战之一。以下是我关于构建真正能转化的多语言游艇网站所学到的一切。

目录

为地中海经纪公司构建多语言游艇网站

为什么地中海游艇经纪公司需要多语言网站

根据盟军市场研究公司的数据,地中海游艇市场预计到 2026 年将达到 123 亿美元。但大多数代理公司忽略的是:这个市场从根本上因语言而分化。一艘在摩纳哥上市的 45 米 Benetti 游艇需要被一位用德语搜索的德国工业家、一位用阿拉伯语浏览的沙特买家以及一位用英语搜索的英国退休者发现。

我见过经纪公司因网站仅提供英文内容而损失六位数的佣金。一位安提比斯的经纪人告诉我,他最大的竞争对手仅因为他们的列表出现在法语 Google 搜索结果中就赢得了法语使用者客户。这不是一个技术问题——这是一个有技术解决方案的商业问题。

数据支持这一点:

市场细分 所需语言 买家人口统计
法国里维埃拉 FR、EN、RU、AR 欧洲高净值个人、中东买家
意大利海岸 IT、EN、DE、FR 北欧包租客户
希腊岛屿 EL、EN、DE、FR 包租为主、季节性旅游
土耳其里维埃拉 TR、EN、DE、RU 预算意识强的包租市场
巴利阿里群岛 ES、EN、DE、FR 混合经纪和包租
克罗地亚海岸 HR、EN、DE、IT 新兴市场,增长迅速

如果你只提供其中的一种或两种语言,你就在将金钱留在桌上。句号。

选择正确的技术栈

对于游艇经纪网站,你需要一个能处理两种非常不同内容类型的栈:静态营销内容(关于页面、服务描述、团队简历)和动态列表数据(游艇规格、定价、可用性、照片)。

我已经用 Next.js 和 Astro 构建过这些,两者都运作良好,具体取决于你的需求。如果你需要重度交互——保存的搜索、比较工具、具有实时可用性的询问表——Next.js 是更合适的选择。如果网站主要是一个展示柜,动态行为较少,Astro 的岛屿架构为你提供了开箱即用的令人难以置信的性能。

以下是栈在这个特定用例中的比较:

功能 Next.js(App Router) Astro Remix
i18n 路由 内置中间件 手动或插件 手动
静态生成 优秀 优秀 有限
动态列表 原生 SSR/ISR 按需端点 原生 SSR
CMS 集成 优秀 优秀 很好
边缘渲染 Vercel Edge、Cloudflare Cloudflare、Netlify Cloudflare
翻译库 next-intl、next-i18next astro-i18n、paraglide remix-i18next
构建时间(500 个列表 × 6 种语言) ~4 分钟(使用 ISR) ~8 分钟(完整静态) N/A(SSR)

对于无头 CMS 层,我强烈建议将你的列表数据与营销内容分离。对列表数据使用专用游艇管理系统(如 Yatco API、NauticEd 或自定义 Supabase 后端),对其他所有内容使用无头 CMS(如 Sanity 或 Contentful)。

为什么无头很重要

游艇数据很奇怪。你有以米或英尺为单位的规格(取决于受众)、以欧元或美元计的价格、不断更新的发动机工作小时数以及每天都在变化的可用性日历。尝试在传统 CMS 中管理所有这些是一场噩梦。无头方法让你可以从专用 API 中提取列表数据,从 CMS 中提取营销内容,然后在构建时或请求时将它们组合在一起。

多语言游艇列表的网址策略

这是大多数项目早期出错的地方。多语言网站的网址结构是最难以后扭转的决定之一。有三种方法:

子目录模式(推荐)

https://yachtbroker.com/en/yachts/benetti-45m-2022
https://yachtbroker.com/fr/yachts/benetti-45m-2022
https://yachtbroker.com/de/yachten/benetti-45m-2022

这是我对 90% 游艇经纪公司的推荐。单一域名、共享域名权限、易于用 Next.js 中间件或 Astro 的内置 i18n 路由实现。

子域名模式

https://en.yachtbroker.com/yachts/benetti-45m-2022
https://fr.yachtbroker.com/yachts/benetti-45m-2022

一些较大的经纪公司出于组织原因偏好这种方式。每个子域可以独立部署。但你会丧失合并的域名权限,需要更多的基础设施管理。

国家代码顶级域名模式

https://yachtbroker.fr/yachts/benetti-45m-2022
https://yachtbroker.de/yachten/benetti-45m-2022

仅当你在每个国家都有单独的法律实体时才有意义。昂贵、复杂,除非你是 Burgess 或 Fraser 级别的运营,否则很少值得。

网址段翻译

以下是让人困惑的细节:你应该翻译网址段吗?对于游艇名称,不应该——保持一致。"Benetti Oasis 40M"在每种语言中都称为这样。但分类路径?是的,翻译那些。

// next.config.js - Next.js i18n 路由
const nextConfig = {
  i18n: {
    locales: ['en', 'fr', 'de', 'it', 'es', 'el'],
    defaultLocale: 'en',
    localeDetection: true,
  },
};

对于 Next.js App Router 中带有 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 });

为地中海经纪公司构建多语言游艇网站 - 架构

翻译游艇列表数据

这是核心挑战。游艇列表有三种内容类型,每种都需要不同的翻译方法:

1. 结构化数据(不翻译,本地化)

长度、宽度、吃水、发动机功率等规格——这些不需要翻译。它们需要本地化。向欧洲人显示米,向美国人显示英尺。向某些市场显示千瓦,向其他市场显示马力。

// utils/localize-specs.ts
const UNIT_PREFERENCES: Record<string, UnitSystem> = {
  en: 'imperial',
  'en-GB': 'metric', // 英国市场对游艇使用米
  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. 枚举字段(使用翻译键)

船体类型、燃料类型、游艇分类——这些是应该使用翻译键而不是自由文本翻译的固定选项。

// 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. 自由文本描述(困难的部分)

游艇描述是营销文案。它们由经纪人编写——通常用英文或法文——并充满行业术语、感情用语和具体声明。对于 500 万欧元的列表,仅机器翻译是不够的。

以下是我推荐的方法:

  1. 在你的 CMS/数据库中存储原始语言描述
  2. 使用 AI 翻译作为第一步——GPT-4o 或 Claude 在 2025 年对游艇术语的处理出奇地好
  3. 标记价格高于阈值的列表(比如 100 万欧元以上)以供人工审查
  4. 缓存翻译的描述,这样你就不会为每次请求都支付重新翻译的费用
// 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;
}

这种方法的成本微乎其微。用 GPT-4o 翻译 500 字的游艇描述大约需要 0.01-0.02 美元。即使有 500 个列表 × 6 种语言,你的初始翻译通过的成本也只有大约 30-60 美元。高级列表的人工审查会增加成本,但当单艘游艇销售产生 50,000-200,000 美元的佣金时,这绝对值得。

i18n 实现模式

让我通过 Next.js App Router 和 next-intl 的实际实现模式来演示,因为这是我们大多数无头 CMS 项目使用的栈。

项目结构

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.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|.*\\..*).*)'],
};

包含翻译的游艇列表页面

// 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>
  );
}

处理货币和单位本地化

地中海的游艇定价几乎总是以欧元列出,但来自不同市场的买家期望看到当地货币的参考价格。以下是我的处理方式:

// utils/localize-price.ts
const CURRENCY_BY_LOCALE: Record<string, string> = {
  en: 'EUR',      // 国际英语在地中海市场中默认为欧元
  'en-US': 'USD',
  fr: 'EUR',
  de: 'EUR',
  it: 'EUR',
  es: 'EUR',
  el: 'EUR',
  tr: 'EUR',      // 土耳其市场仍以欧元定价
  ar: '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);
}

重要注意:当显示转换后的价格时,始终显示"以欧元计价"或等效免责声明。游艇销售合同以特定货币计价,在不提供上下文的情况下显示转换后的价格可能会造成法律问题。

多语言游艇网站的 SEO

这是真正的回报所在。正确的多语言 SEO 意味着你的 Azimut 68 列表在有人在慕尼黑搜索"Azimut 68 kaufen"时出现,也在有人在巴黎搜索"Azimut 68 à vendre"时出现。

hreflang 标签

这是必不可少的。每个页面都需要 hreflang 标签指向所有语言版本:

<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" />

每种语言的结构化数据

对每个语言版本使用本地化描述的 Product 架构。Google 明确支持特定语言的结构化数据,它有助于你的列表在不同 Google 域名中的富文本结果中出现。

站点地图策略

生成每种语言的单独站点地图,并从站点地图索引中引用它们:

<!-- 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>

性能考虑

一个有 30 多张高分辨率照片、翻译内容和本地化规格的游艇列表页面可能很快就会变得很重。以下是重要的事项:

  • ISR(增量静态再生):每 60 分钟再生一次列表页面。游艇列表不会按秒变化,但定价和可用性可能每天发生变化。
  • 翻译缓存:永远不要翻译相同的描述两次。使用 Redis 或甚至简单的数据库表来缓存翻译。
  • 图像优化:这通常是最大的胜利。单个游艇画廊可能包含 2GB 的源图像。使用 Next.js Image 或带有自动格式协商的 CDN(WebP/AVIF)。
  • 按语言的包分割:不要为英语用户加载法语翻译。next-intlparaglide 都自动处理这个问题。

在最近的一个项目中,这些优化将我们的所有语言中最大内容绘制从 4.2 秒降低到 1.1 秒。当你的跳出率直接与损失的佣金相关时,这很重要。

真实世界架构示例

以下是我们用于地中海经纪网站的架构:

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Sanity     │     │  Yacht API   │     │  Redis      │
│  (Marketing  │     │  (Listings,  │     │  (Translation│
│   content)   │     │   specs)     │     │   cache)    │
└──────┬───────┘     └──────┬───────┘     └──────┬──────┘
       │                    │                    │
       └────────────┬───────┘────────────────────┘
                    │
            ┌───────▼───────┐
            │   Next.js     │
            │   App Router  │
            │   + next-intl │
            └───────┬───────┘
                    │
            ┌───────▼───────┐
            │   Vercel      │
            │   Edge Network│
            └───────────────┘

Sanity 处理营销页面、团队简历、博客文章——所有这些都具有原生多语言支持。游艇 API(第三方服务或自定义 Supabase 后端)提供列表数据。Redis 缓存 AI 生成的翻译。Next.js 用区域设置感知路由将所有这些结合在一起。

如果这种架构听起来像是你需要的,我们很乐意讨论你的项目。我们已经为几家地中海经纪公司构建了其中的一些,模式已经得到了完善。

常见问题

一个地中海游艇网站应该支持多少种语言? 至少你需要英语加上你主要市场的当地语言。对于认真的经纪公司,我建议英语、法语、德语和意大利语作为基础——这涵盖了大约 80% 的地中海游艇买家。如果你的目标是 500 万欧元以上的超豪华细分市场,请添加俄语和阿拉伯语。

我应该对游艇列表使用机器翻译还是聘请专业翻译? 两者都用。对所有列表使用 AI 翻译(GPT-4o 或 Claude)作为第一步,然后让人工翻译者审查价格高于你阈值的列表。对于 500 字的描述,AI 翻译成本不到 0.02 美元,让你完成 90% 的工作。高级列表的人工审查每份描述成本 20-50 美元,但确保了高价值销售的准确性。

最好的多语言游艇网站 CMS 是什么? Sanity 和 Contentful 都开箱即用地很好地处理多语言内容。Sanity 的文档级本地化给你更多的灵活性,而 Contentful 的字段级本地化更容易设置。对于游艇列表数据本身,我建议使用单独的专用系统,而不是尝试将所有内容强制放入通用 CMS 中。查看我们的无头 CMS 开发页面了解更多详情。

我如何在不同的单位系统中处理游艇测量? 在你的数据库中以公制(米、千瓦)存储所有测量。仅在显示层根据用户的语言环境转换为英制。游艇行业在欧洲普遍使用公制,但美国买家期望英尺和马力。使用 Intl.NumberFormat API 进行一致的格式化。

hreflang 标签对游艇 SEO 真的重要吗? 绝对重要。没有 hreflang 标签,Google 可能会向德国搜索者显示你的法语列表,或者更糟的是,将你的翻译页面视为重复内容。我们在之前做错 hreflang 的经纪网站上正确实现后,看到自然流量增加了 40-60%。

构建一个多语言游艇经纪网站要花多少钱? 一个正确构建的多语言游艇网站,包含 4-6 种语言、CMS 集成和游艇列表管理,通常需要 30,000-80,000 美元,具体取决于复杂性。最大的成本驱动因素是语言数量、自定义搜索/过滤功能和与现有游艇管理系统的集成。访问我们的定价页面了解更具体的估计。

我以后可以向我的游艇网站添加语言吗? 是的,如果它从一开始就正确构建。使用适当的 i18n 架构,添加新语言意味着创建新的翻译文件、翻译你的静态 UI 字符串,以及通过翻译管道运行你的列表描述。路由和基础设施应该已经处理了它。如果你当前的网站一开始就没有考虑 i18n 构建,改造会更难——但仍然可行。

那么阿拉伯语之类的从右到左语言呢? 阿拉伯语对于地中海游艇销售越来越重要,特别是在 1000 万欧元以上的细分市场中。你的 CSS 需要支持 RTL 布局——使用逻辑属性(margin-inline-start 而不是 margin-left)并进行彻底测试。Next.js 支持 RTL,通过每个语言环境切换 HTML 元素上的 dir 属性。它增加了开发时间,但中东买家代表了一个重要且快速增长的市场细分。