[Astro] 多言語(i18n)実装

2026-03-27 hit count image

Astroで日本語、韓国語、英語の3言語をサポートする多言語システムを実装する方法を共有します。URLベースルーティング、翻訳システム、動的ルーティングなどを扱います。

astro

概要

前回の記事Content Collectionsとマークダウンマイグレーションでコンテンツ管理方法を扱いました。今回の記事では、このブログで日本語、韓国語、英語の3言語をサポートするために実装した多言語(i18n)システムを詳しく説明します。

Astroには公式i18nライブラリがありませんが、ファイルベースルーティングとシンプルな翻訳システムを組み合わせることで効果的な多言語サイトを構築できます。

多言語ルーティング戦略

URL構造

このブログはURLプレフィックスベースのルーティングを使用しています。

言語URLパターン
日本語(デフォルト)/path//astro/installation/
韓国語/ko/path//ko/astro/installation/
英語/en/path//en/astro/installation/

デフォルト言語の日本語はURLに言語プレフィックスがなく、韓国語と英語にはそれぞれ/ko//en/プレフィックスが付きます。

ディレクトリベースの実装

Astroのファイルベースルーティングを活用してディレクトリ構造で多言語を実装します。

src/pages/
├── index.astro              # 日本語ホーム
├── [...path].astro          # 日本語ポスト(catch-all)
├── latest/
│   └── index.astro          # 日本語最新記事
├── search/
│   └── index.astro          # 日本語検索
├── ko/
│   ├── index.astro          # 韓国語ホーム
│   ├── [...path].astro      # 韓国語ポスト
│   ├── latest/
│   │   └── index.astro      # 韓国語最新記事
│   └── search/
│       └── index.astro      # 韓国語検索
└── en/
    ├── index.astro          # 英語ホーム
    ├── [...path].astro      # 英語ポスト
    ├── latest/
    │   └── index.astro      # 英語最新記事
    └── search/
        └── index.astro      # 英語検索

各言語ディレクトリに同じ構造のページファイルを置いて、言語別に独立したルーティングを実装します。

翻訳システムの実装

UIテキストの翻訳はsrc/i18n/translations.tsで一元管理します。

言語タイプ定義

// src/i18n/translations.ts
export const defaultLang = 'ja';
export const languages = { ja: '日本語', ko: '한국어', en: 'English' } as const;
export type Lang = keyof typeof languages;
  • defaultLang:デフォルト言語を日本語に設定
  • languages:サポート言語一覧。言語切替UIで使用
  • Lang:TypeScriptタイプとして'ja' | 'ko' | 'en'

翻訳レコード

export const translations: Record<Lang, Record<string, string>> = {
  ja: {
    'nav.home': 'Home',
    'nav.latest': 'Latest',
    'nav.category': 'Category',
    'latest.title': '最新記事',
    'search.title': '検索',
    'search.placeholder': '検索キーワードを入力...',
    'post.commentEncouragement':
      '私のブログが役に立ちましたか?下にコメントを残してください。',
    'footer.share': 'SHARE',
    'home.title': 'プログラミングでアートを夢見る.',
    // ...
  },
  ko: {
    'nav.home': 'Home',
    'nav.latest': 'Latest',
    'nav.category': 'Category',
    'latest.title': '최신글',
    'search.title': '검색',
    'search.placeholder': '검색어를 입력하세요...',
    'post.commentEncouragement':
      '제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!',
    'footer.share': 'SHARE',
    'home.title': '프로그래밍으로 예술을 꿈꾼다',
    // ...
  },
  en: {
    // ...
  },
};

キー名はセクション.項目形式のドット記法を使用して構造的に管理します。このドット記法を活用すると、ナビゲーション(nav.)、検索(search.)、ポスト(post.)など領域別に区分できます。

ヘルパー関数

翻訳システムを便利に使うためのヘルパー関数です。

getLangFromUrl — URLから言語を検出

export function getLangFromUrl(url: URL): Lang {
  const path = url.pathname;
  if (path.startsWith('/ko/') || path === '/ko') return 'ko';
  if (path.startsWith('/en/') || path === '/en') return 'en';
  return 'ja';
}

URLパスのプレフィックスを確認して現在の言語を判別します。/ko/または/en/で始まらなければデフォルト言語(日本語)を返します。

useTranslations — 翻訳関数の生成

export function useTranslations(lang: Lang) {
  return function t(key: string): string {
    return translations[lang][key] || translations[defaultLang][key] || key;
  };
}

言語を受け取り翻訳関数t()を返すクロージャパターンです。該当言語の翻訳がなければデフォルト言語(日本語)の翻訳を使用し、それもなければキー自体を返します。

コンポーネントでは以下のように使用できます。

---
// コンポーネントのfrontmatter領域
import { useTranslations } from '../i18n/translations';
const t = useTranslations('ja');
---

<h1>{t('home.title')}</h1>
<!-- 出力: プログラミングでアートを夢見る. -->

getLocalizedPath — 言語別パス生成

export function getLocalizedPath(path: string, lang: Lang): string {
  // 既存の言語プレフィックスを削除
  const cleanPath = path
    .replace(/^\/(ko|en)\//, '/')
    .replace(/^\/(ko|en)$/, '/');
  if (lang === defaultLang) return cleanPath;
  return `/${lang}${cleanPath}`;
}

パスに言語プレフィックスを追加する関数です。デフォルト言語(日本語)にはプレフィックスを付けません。

getLocalizedPath('/astro/installation/', 'ja'); // → '/astro/installation/'
getLocalizedPath('/astro/installation/', 'ko'); // → '/ko/astro/installation/'
getLocalizedPath('/astro/installation/', 'en'); // → '/en/astro/installation/'

言語切替リンクを生成する時に便利です。例えば、Navbarで言語切替ボタンを作る時に現在のページの他の言語バージョンのURLを生成するのに使用します。

動的ルーティングと言語別ページ

ブログポストは[...path].astroを使用してcatch-allルーティングで処理します。各言語別に別々のルーティングファイルを持ちます。

日本語ポストルーティング

---
// src/pages/[...path].astro
import type { GetStaticPaths } from 'astro';
import PostLayout from '../layouts/PostLayout.astro';
import { getCollection, render } from 'astro:content';
import { getLocalizedPath } from '../i18n/translations';

const lang = 'ja';

export const getStaticPaths = (async () => {
  const posts = await getCollection('blog', (post) =>
    post.data.lang === 'ja' && post.data.published !== false
  );

  return posts.map((post) => {
    const pathStr = post.data.permalink.replace(/^\//, '').replace(/\/$/, '');
    return {
      params: { path: pathStr },
      props: { post },
    };
  });
}) satisfies GetStaticPaths;

const { post } = Astro.props;
const { Content } = await render(post);
---

<PostLayout
  title={post.data.title}
  description={post.data.description}
  image={post.data.image}
  lang={lang}
  permalink={getLocalizedPath(post.data.permalink, lang)}
  category={post.data.category}
  date={post.data.date.toISOString()}
  comments={post.data.comments}
>
  <Content />
</PostLayout>

動作原理

  1. getStaticPaths()で該当言語(ja)の公開済みポストをすべて取得します
  2. 各ポストのpermalinkから前後の/を除去してparams.pathとして使用します
  3. ビルド時に各ポストに対して静的HTMLファイルが生成されます
  4. getLocalizedPath()で言語プレフィックスが含まれたURLをPostLayoutに渡します

例えば、permalink: /astro/installation/の日本語ポストは:

  • params.pathastro/installationになり
  • 最終URLが/astro/installation/になります

韓国語はsrc/pages/ko/[...path].astroで、英語はsrc/pages/en/[...path].astroで処理します。構造は同じでlangの値だけが異なります。

言語別Google Fonts

各言語に適したフォントをロードするためにHead.astroで言語別Google Fontsを設定します。

---
// Head.astro 一部
const fontUrl = lang === 'ja'
  ? 'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'
  : lang === 'ko'
    ? 'https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap'
    : 'https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap';
---

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href={fontUrl} media="print" onload="this.media='all'" />
  • 日本語:Noto Sans JP
  • 韓国語:Noto Sans KR
  • 英語:Noto Sans

media="print" + onload="this.media='all'"テクニックでフォントを非同期ロードしてレンダリングブロッキングを防止します。

サイトマップi18n設定

@astrojs/sitemapプラグインで以下のように設定して多言語サイトマップを生成します。

// astro.config.mjs
sitemap({
  serialize(item) {
    item.lastmod = new Date();
    return item;
  },
  i18n: {
    defaultLocale: 'ja',
    locales: {
      ja: 'ja',
      ko: 'ko',
      en: 'en',
    },
  },
}),

この設定で生成されるサイトマップにはhreflangタグが含まれ、検索エンジンが各ページの言語別バージョンを認識できます。

Jekyllとの比較

Jekyllではjekyll-multiple-languages-pluginを使用して多言語を実装していました。2つの方式の違いを比較します。

項目JekyllAstro
多言語方式プラグイン依存(jekyll-multiple-languages-plugin自前実装(ファイルベースルーティング)
翻訳ファイルYAMLファイル(_i18n/ja.ymlTypeScriptファイル(translations.ts
型安全性なしTypeScriptタイプサポート
URLパターンプラグイン設定に依存ディレクトリ構造で直接制御
フォールバックプラグイン機能useTranslations関数で処理
サイトマップ別途設定が必要@astrojs/sitemap i18n設定

Astroでは外部プラグインなしにファイルベースルーティングとシンプルなTypeScript関数だけで多言語を実装できるため、より柔軟でメンテナンスが容易です。

詳細は以前に作成した多言語対応プラグインポストを参考にしてください。

完了

今回の記事ではAstroで多言語システムを実装する方法を見てきました。

  • URLプレフィックスベースルーティングで3言語サポート(//ko//en/
  • translations.tsでUI翻訳を一元管理
  • getLangFromUrluseTranslationsgetLocalizedPathヘルパー関数
  • [...path].astro catch-allルーティングで言語別ポストページ生成
  • 言語別Google Fontsの非同期ロード
  • サイトマップi18n設定

次の記事SEO実装では、検索エンジン最適化のためのメタタグ、構造化データ、サイトマップなどの実装方法を扱います。

シリーズ案内

このポストはJekyllからAstroへのマイグレーションシリーズの一部です。

  1. JekyllからAstroへマイグレーションした理由
  2. Astroのインストールとプロジェクト構成
  3. Content Collectionsとマークダウンマイグレーション
  4. 多言語(i18n)実装
  5. SEO実装
  6. 画像最適化 — カスタムrehypeプラグイン
  7. コメントシステム(Utterances)
  8. 広告連携(Google AdSense)
  9. Pagefindを利用した検索実装
  10. レイアウトとコンポーネントアーキテクチャ
  11. GitHub Pagesデプロイ
  12. ソーシャル共有自動化スクリプト
  13. トラブルシューティングとヒント

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。



SHARE
Twitter Facebook RSS