昨年、地中海全域で6つの言語でリスティングを提供する必要があるヨットブローカープラットフォームを構築しました。カンヌのフランス人バイヤー、ポルト・チェルボのイタリア人クライアント、アテネのギリシャ人チャーター希望者、ボドルムのトルコ人マリーナオペレーター、パルマ・デ・マヨルカのスペイン人ブローカー、そしてその間に散在する英語話者の駐在員。「翻訳ボタンを追加するだけ」から始まったこのプロジェクトは、これまで取り組んだ中で最も興味深いi18nの課題の1つになりました。実際にコンバージョンする多言語ヨットウェブサイトの構築について学んだすべてをここにご紹介します。

目次

地中海ブローカー向け多言語ヨットウェブサイトの構築

地中海ヨットブローカーがなぜ多言語サイトを必要とするのか

Allied Market Researchによれば、地中海ヨット市場は2026年までに123億ドルに達すると予測されています。しかし、ほとんどのエージェンシーが見落としているのはこの市場が本質的に言語によって分断されているという点です。モナコに登録された45メートルのベネッティは、ドイツ語で検索するドイツの実業家、アラビア語で閲覧するサウジアラビアのバイヤー、そして英語で探しているイギリスの退職者によって発見可能である必要があります。

英語コンテンツのみを提供していたためにブローカーが6桁の仲介手数料を失うのを見たことがあります。アンティベスのあるブローカーは、彼の最大のライバルが単にフランス語Googleの結果でリスティングが表示されているというだけでフランス語を話すクライアントを獲得していると語っていました。これはテクノロジーの問題ではなく、テクノロジーソリューションを備えたビジネスの問題です。

数字がこれを裏付けています:

市場セグメント 必要言語 バイヤーの人口統計
フレンチリビエラ FR、EN、RU、AR ヨーロッパのHNWI、中東のバイヤー
イタリア沿岸 IT、EN、DE、FR 北ヨーロッパのチャータークライアント
ギリシャ諸島 EL、EN、DE、FR チャーター中心、季節観光
トルコ沿岸 TR、EN、DE、RU 予算重視のチャーター市場
バレアレス諸島 ES、EN、DE、FR ブローカーとチャーターの混在
クロアチア沿岸 HR、EN、DE、IT 新興市場、急速に成長中

1つか2つの言語のみを提供している場合、テーブルにお金を残しています。期間です。

適切なテックスタックの選択

ヨットブローカーサイトでは、静的マーケティングコンテンツ(アバウトページ、サービス説明、チームバイオ)と動的リスティングデータ(ヨット仕様、価格設定、利用可能性、写真)という2つの異なるコンテンツタイプを処理するスタックが必要です。

これらはNext.jsとAstroの両方で構築してきましたが、要件に応じてどちらも同様に機能します。保存検索、比較ツール、リアルタイム利用可能性を備いた問い合わせフォームなど、重い相互作用が必要な場合、Next.jsが適切です。サイトが主にショーケースであまり動的な動作がない場合、Astroのアイランドアーキテクチャは驚異的なパフォーマンスをすぐに提供します。

このユースケースに対するスタックの比較は以下の通りです:

機能 Next.js(App Router) Astro Remix
i18nルーティング 組み込み middleware マニュアルまたはプラグイン マニュアル
静的生成 優秀 優秀 限定的
動的リスティング ネイティブSSR/ISR オンデマンドエンドポイント ネイティブSSR
CMS統合 優秀 優秀 良好
エッジレンダリング Vercel Edge、Cloudflare Cloudflare、Netlify Cloudflare
翻訳ライブラリ next-intl、next-i18next astro-i18n、paraglide remix-i18next
ビルド時間(500リスティング×6言語) ISR付き約4分 完全静的約8分 N/A(SSR)

headless CMSレイヤーに対しては、リスティングデータをマーケティングコンテンツから分離することを強く推奨します。専用ヨット管理システム(Yatco API、NauticEd、またはカスタムSupabaseバックエンド)をリスティングデータに使用し、その他すべてについてはSanityやContentfulなどのheadless CMSを使用してください。

Headlessがここで重要な理由

ヨットデータは奇妙です。メートルまたはフィート(観客によって異なります)での仕様、ユーロまたはドル、常に更新されるエンジン時間、毎日変わる利用可能性カレンダーがあります。すべてを従来的なCMS内で管理しようとするのは悪夢です。Headlessアプローチを使用すると、専用APIからリスティングデータを取得し、CMSからマーケティングコンテンツを取得してから、ビルド時またはリクエスト時にそれらを組み合わせることができます。

多言語ヨットリスティングのURL戦略

これは最初のほとんどのプロジェクトが間違える場所です。多言語サイトのURL構造は、後で逆転させるのが最も難しい決定の1つです。3つのアプローチがあります:

サブディレクトリパターン(推奨)

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 middlewareまたはAstroの組み込みi18nルーティングで実装が簡単です。

サブドメインパターン

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

より大きなブローカーは組織的な理由でこれを好みます。各サブドメインは独立してデプロイできます。しかし、統合されたドメインオーソリティが失われ、管理するインフラストラクチャがもっと多くなります。

ccTLDパターン

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

各国に別々の法人がある場合にのみ意味があります。高額で複雑であり、Burgessまたはフレーザーレベルの業務でない限り、ほとんど価値がありません。

スラッグの翻訳

ここに人々を混乱させる詳細があります:URLスラッグを翻訳すべきですか?ヨット名の場合、いいえ。一貫性を保ってください。「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 });

地中海ブローカー向け多言語ヨットウェブサイトの構築 - アーキテクチャ

ヨットリスティングデータの翻訳

これが中核的な課題です。ヨットリスティングには、それぞれ異なる翻訳アプローチが必要な3種類のコンテンツがあります:

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. 価格のしきい値を超えるリスティングにフラグを立てます(たとえば、€1M以上)、人間によるレビューのために
  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;
}

このアプローチのコストは最小限です。500語のヨット説明をGPT-4oで翻訳するコストは約$0.01-0.02です。500リスティング×6言語でさえ、最初の翻訳パスで約$30-60かかります。プレミアムリスティングの人間によるレビューはコストを追加しますが、単一のヨット販売が$50K-200Kの仲介手数料を生成する場合、それは絶対に価値があります。

i18n実装パターン

Next.js App Routerとnext-intlを使用した実装パターンについて説明しましょう。これはほとんどのheadless 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

// 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',      // 国際英語はMed市場ではEURがデフォルト
  'en-US': 'USD',
  fr: 'EUR',
  de: 'EUR',
  it: 'EUR',
  es: 'EUR',
  el: 'EUR',
  tr: 'EUR',      // トルコ市場はまだEURで価格を設定しています
  ar: 'USD',      // 中東のバイヤーは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);
}

重要な注意:換算価格を表示する場合は必ず「EURでの価格」または同等の免責事項を表示してください。ヨット売却契約は特定の通貨で表示され、コンテキストなしに変換された価格を表示すると法的問題が発生する可能性があります。

多言語ヨットウェブサイトの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分ごとに再生成します。ヨットリスティングは秒単位では変わりませんが、価格と可用性は毎日変動することができます。
  • 翻訳キャッシング: 同じ説明を2回翻訳しないでください。Redisを使用するか、単純なデータベーステーブルを使用して翻訳をキャッシュします。
  • 画像の最適化: これはしばしば最大の勝利です。単一のヨットギャラリーには2GBのソースイメージが含まれる可能性があります。Next.js ImageまたはWebP/AVIF自動フォーマットネゴシエーション付きCDNを使用してください。
  • ロケール別のバンドル分割: 英語ユーザーにフランス語翻訳を読み込まないでください。next-intlparaglideの両方がこれを自動的に処理します。

最近のプロジェクトでは、これらの最適化により、すべてのロケール全体でLargest Contentful Paintが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はキャッシュします生成された翻訳。Next.jsはロケール対応ルーティングですべてを結びつけます。

このようなアーキテクチャがあなたが必要とするもののように聞こえるなら、プロジェクトについて話し合いたいです。私たちは地中海ブローカーのために同様のものを複数構築しており、パターンを調整しました。

FAQ

地中海ヨットウェブサイトはいくつの言語をサポートすべきですか? 最低限、英語とあなたの主要市場の現地言語が必要です。真摯なブローカーの場合、基本線として英語、フランス語、ドイツ語、イタリア語を推奨します。これは地中海ヨットバイヤーのおよそ80%をカバーします。€5Mを超えるウルトララグジュアリーセグメントをターゲットにしている場合、ロシア語とアラビア語を追加します。

ヨットリスティングに機械翻訳または専門翻訳者を使用すべきですか? どちらも。すべてのリスティングに対してAI翻訳(GPT-4oまたはClaude)を最初のパスとして使用し、その後、価格のしきい値を超えるリスティングを人間の翻訳者にレビューしてもらいます。500語の説明の場合、AI翻訳のコストは$0.02未満であり、そこまで90%の道を進みます。プレミアムリスティングの人間によるレビューは説明あたり$20-50かかりますが、高額な販売の精度を保証します。

多言語ヨットウェブサイト向けの最適なCMSは何ですか? SanityとContentfulの両方は、すぐに多言語コンテンツを適切に処理します。Sanityのドキュメントレベルのローカライゼーションは、より多くの柔軟性を与えます。一方、Contentfulのフィールドレベルのローカライゼーションはセットアップが簡単です。ヨットリスティングデータ自体については、すべてを汎用CMSに無理やり入れようとするのではなく、専用システムを推奨します。詳細については、headless CMS開発ページを確認してください。

ユーザーのロケールに基づいてヨットの測定値を異なるユニットシステムで処理するにはどうすればよいですか? データベースのすべての測定値をメートル(メートル、キロワット)で保存します。ユーザーのロケールに基づいて表示レイヤーのみで帝国に変換します。ヨット業界はヨーロッパで普遍的にメートル法を使用していますが、アメリカのバイヤーはフィートと馬力を期待しています。一貫した書式設定のためにIntl.NumberFormat APIを使用してください。

hreflangタグは本当にヨットSEOに重要ですか? 絶対に。hreflangタグがない場合、Googleはあなたのフランス語リスティングをドイツの検索者に表示する可能性があります。またはさらに悪いことに、翻訳されたページを重複コンテンツとして扱う可能性があります。以前それを間違えていたブローカーサイト全体にhreflangを適切に実装した後、オーガニック搜索トラフィックが40~60%増加しているのを見てきました。

多言語ヨットブローカーウェブサイトを構築するのにどのくらいの費用がかかりますか? 4~6言語、CMS統合、およびヨットリスティング管理と適切に構築された多言語ヨットサイトは、複雑さに応じて通常$30,000~80,000で実行されます。最大のコストドライバーは言語の数、カスタム検索/フィルター機能、および既存のヨット管理システムとの統合です。より具体的な見積もりについては、価格設定ページにアクセスしてください。

後でヨットウェブサイトに言語を追加できますか? はい、最初から正しく構築されている場合。適切なi18nアーキテクチャを使用すれば、新しい言語の追加は新しい翻訳ファイルの作成、静的UI文字列の翻訳、そしてリスティング説明を翻訳パイプラインを通して実行することを意味します。ルーティングとインフラストラクチャはすでにそれを処理できるはずです。現在のサイトがi18nの観点から構築されていない場合、改造は難しくなります。しかし、それでもできます。

アラビア語のような右から左への言語はヨットウェブサイトではどうですか? アラビア語は地中海ヨット販売、特に€10M以上のセグメントで徐々に重要になっています。CSSはRTLレイアウトをサポートする必要があります — 論理的プロパティを使用してください(margin-leftの代わりにmargin-inline-start)そして徹底的にテストしてください。Next.jsはロケールごとに切り替えられるHTMLの要素にあるdir属性でRTLをサポートしています。開発時間を追加しますが、中東のバイヤーは重要で急速に成長している市場セグメントです。