多言語サイトを作る — locale で言語別に取得

中級 約15分 最終更新 2026-10-07 Astro

有料オプション「多言語オプション」を有効にしたサイトでは、公開APIのコンテンツ取得系メソッドに言語コードを渡すだけで、言語別の公開版を取得できます。このガイドでは、対応言語と locale の渡し方、未翻訳コンテンツの原文フォールバック(translation_available)、そして静的サイト(SSG)で言語別ページを焼く設計までをまとめます。

前提:この機能は、運営側で多言語オプションが有効化され、対象言語が設定されているサイトでのみ使えます。無効なサイトや未設定・未対応の言語を指定すると、公開APIは 400(isBadRequest・E001008)を返します。locale は @sharedan/cms-sdk 0.7 以上で渡せます。

1. 多言語オプションと対応言語

取得できる言語は en(英語)・zh-Hans(中国語簡体)・zh-Hant(中国語繁体)・ko(韓国語)・vi(ベトナム語)・fr(フランス語)・de(ドイツ語)です。原文は日本語(ja)で、原文を取得するときは locale を指定しません(省略します)。実際に出せる言語は、運営側でサイトごとに設定した言語に限られます。

翻訳対象は、投稿・FAQ・固定コンテンツの本文、タイトル、カテゴリ名、FAQの質問/回答、メタタグ/OGPのテキスト、フォーム定義(項目ラベル等)、メンテナンス状態(title/message)です。slug・key などの識別子、投稿の published_at、メンテナンスの resumes_at は言語共通(翻訳しません)。

2. locale を渡して取得する

取得したい言語コードを locale に渡すだけです。posts.list/posts.get/posts.categories/faqs.list/faqs.categories/contents.get/forms.get/maintenance.get が対応します。

言語別の取得
import { cms } from '../../lib/cms';
import { renderProseMirror } from '@sharedan/cms-sdk';

// 投稿一覧・個別(本文も指定言語で返る)
const { items } = await cms.posts.list('news', { locale: 'en', perPage: 20 });
const post = await cms.posts.get('news', slug, { locale: 'en' });
const html = post.body ? renderProseMirror(post.body) : '';

// FAQ・カテゴリ・固定コンテンツ・フォーム定義・メンテナンス状態も同じ要領
const faqs = await cms.faqs.list({ locale: 'ko' });
const cats = await cms.posts.categories('news', { locale: 'ko' }); // slug は言語共通
const company = await cms.contents.get('company-message', { locale: 'zh-Hant' });
const form = await cms.forms.get('contact', { locale: 'vi' });
const maint = await cms.maintenance.get({ locale: 'en' });
ヒント:カテゴリ名・フォーム定義・メンテナンス状態は常に自動翻訳されます(手動の言語別編集はありません)。投稿・FAQ・固定コンテンツの本文は、管理画面で言語別に編集した公開版があればその訳を返します。

3. 未翻訳のフォールバック(translation_available)

公開APIは常に各言語の公開版だけを返します。指定した言語の公開版がまだ無いコンテンツは、エラーにはせず原文(日本語)を返し、レスポンスに translation_available: false(訳を返したときは true)を含めます。このフラグは localeを指定したときだけ付きます(原文取得時は省略=undefined)。

src/lib/post.ts
const { items } = await cms.posts.list('news', { locale: 'en' });

for (const p of items) {
  if (p.translation_available === false) {
    // 英語版がまだ無い投稿。原文(日本語)が入っている。
    // 「翻訳準備中」の注記を添えて原文を見せる、などの出し分けに使える。
  }
}
// locale を指定しない(=原文取得)と translation_available は付かない(undefined)。

これにより、まだ訳が無い記事も空にならず原文で表示できます。原文のまま見せるか、「翻訳準備中」の注記を添えるかを、このフラグで出し分けてください。

4. 言語別ページを焼く(SSG)

静的サイトでは、言語切替をビルド時に言語別の静的ページとして焼くのが基本です(/=原文 ja、/en/、/zh-Hant/ …)。切替はページ遷移(<a> リンク)で行い、実行時の再取得を挟まないので、静的ビルドの速さをそのまま保てます。

まず、ビルド時に「生成してよい言語」を見極めます。多言語の設定はサイトごとに異なり、まだ有効化していない言語を指定すると 400(E001008)になるため、各候補言語を軽く問い合わせて、有効な言語だけを残します。これで設定差(未有効化の言語)でビルドを落とさずに済みます。

src/lib/locales.ts
// src/lib/locales.ts — ビルド時に「生成してよい言語」を見極める
import { ShareDanCmsApiError } from '@sharedan/cms-sdk';
import { cms } from './cms';

// 出したい候補(APIに渡す locale コード)。原文 ja はルートで別途生成するので含めない。
const CANDIDATES = ['en', 'zh-Hant'];

// 多言語が有効で、かつ運営管理画面で設定済みの言語だけを残す。
export async function resolveExtraLocales() {
  const enabled = [];
  for (const locale of CANDIDATES) {
    try {
      const { items } = await cms.posts.list('news', { perPage: 1, locale });
      // 多言語が有効な公開APIは locale 指定時のレスポンスに translation_available を必ず含める。
      // 付いていなければ(未有効化・旧API)その言語はスキップする。
      if (items[0] && 'translation_available' in items[0]) enabled.push(locale);
    } catch (e) {
      // 無効/未対応の言語は 400(E001008)。その言語は生成しない。
      if (e instanceof ShareDanCmsApiError && e.isBadRequest) continue;
      throw e; // それ以外の実エラーはビルドを止める(壊れたサイトを配信しない)
    }
  }
  return enabled; // 例: ['en', 'zh-Hant'](原文 ja は常にルートで生成)
}

原文(ja)はルート直下のページ(src/pages/posts/news/[slug].astro・localeなしの get())で焼き、原文以外は src/pages/[locale]/… 配下で言語ごとに焼きます。getStaticPaths で「有効言語 × 投稿スラッグ」を列挙するだけです。

src/pages/[locale]/posts/news/[slug].astro
---
// src/pages/[locale]/posts/news/[slug].astro(原文以外の言語ページ)
import { cms } from '../../../../lib/cms';
import { renderProseMirror } from '@sharedan/cms-sdk';
import { resolveExtraLocales } from '../../../../lib/locales';

export async function getStaticPaths() {
  const locales = await resolveExtraLocales();   // ['en', 'zh-Hant'] など(原文 ja は含まない)
  const paths = [];
  for (const locale of locales) {
    const { items } = await cms.posts.list('news', { perPage: 100, locale });
    for (const p of items) {
      paths.push({ params: { locale, slug: p.slug }, props: { locale } });
    }
  }
  return paths;
}

const { locale } = Astro.props;
const post = await cms.posts.get('news', Astro.params.slug, { locale });
const untranslated = post.translation_available === false;
const html = post.body ? renderProseMirror(post.body) : '';
---
<article>
  <h1>{post.title}</h1>
  {untranslated && <p class="untranslated">この記事はまだ翻訳されていません。原文を表示しています。</p>}
  <div set:html={html} />
</article>

言語スイッチャは、同じ記事の各言語版URLへのリンクを並べるだけです(原文はルート、他言語は /{locale}/… のプレフィックス付き)。

src/components/LangSwitcher.ts
// 原文 ja はルート(/…)、他言語は /{locale}/… に置く。
// 同じ記事の各言語版へのリンクを組み立てる。
const localeLink = (code, slug) =>
  code === 'ja' ? '/posts/news/' + slug : '/' + code + '/posts/news/' + slug;

const langs = [
  { code: 'ja', label: '日本語' },
  { code: 'en', label: 'EN' },
  { code: 'zh-Hant', label: '繁中' },
];
実例:ShareDan のサンプルサイト(apps/samples/html)が、この「ビルド時に言語別ページを焼く」構成(/=ja・/en/・/zh-Hant/)と、上記の有効言語プローブをそのまま実装しています。多言語オプションが無効なサイトでは、自動的に日本語のみのサイトとして成立します(graceful degradation)。

5. 注意点

  • パスワード保護された本文も locale で訳される:cms.posts.unlock() にも任意の locale(第4引数)を渡せます。個別取得と同じ言語を渡すと、ゲートされた本文も指定言語の公開版で返り、タイトルと本文の言語が揃います。未訳の本文は原文(日本語)を返し translation_available: false を含めます(パスワード保護された投稿を表示する)。
  • locale はキャッシュキーに含まれます:公開APIは Accept-Language では出し分けません。必ずクエリの localeで指定してください。言語別ページは別URLとして焼かれるため、CDNキャッシュ/パージも言語ごとに独立します。
  • 識別子は訳さない:slug・key・カテゴリの slug は言語共通です。言語別ページでも、ルーティングやカテゴリ絞り込みのキーは原文と同じ値を使えます。
  • 無効・未対応の言語は 400(E001008):多言語オプションが無効なサイトで locale を指定した場合や、設定していない言語を指定した場合に返ります。SSGでは上記のプローブで事前に弾き、実行時に直接叩く構成では isBadRequest を捕捉してください。
FEEDBACK

このページはお役に立ちましたか?

改善要望・不具合・ドキュメントの分かりにくい点など、お気軽にお寄せください。いただいた内容は今後の改善に役立てます。

返信をご希望の場合のみご記入ください。

スパム対策(レートリミット・ハニーポット・送信速度チェック)はサーバ側で自動適用されます。