【 Astro / TailWind CSS 】多言語対応Localization 静的サイトジェネレーター SSG

Astro / TailWind

多言語対応したサイトを作ってみます!

umi Camera | vintage film camera
https://umi.grtlab.com/

フィルムカメラ調の撮影カメラ、iPhone無料アプリです!

アプリ側では15言語対応しているので seo的にも 紹介サイトを多言語化しておこうかなと
今は日本語と英語だけなのですが

プロジェクト作成 umi-astro

 % npm create astro@latest umi-astro
Need to install the following packages:
create-astro@4.13.2
Ok to proceed? (y) y
 tmpl   How would you like to start your new project?
         A basic, helpful starter project

  deps   Install dependencies?
         Yes

   git   Initialize a new git repository?
         Yes
 ✔  Project initialized!
         ■ Template copied
         ■ Dependencies installed
         ■ Git initialized

  next   Liftoff confirmed. Explore your project!

umi-astroディレクトリに作ります

npm installして起動確認

# 1. プロジェクトディレクトリに移動 (もし、まだ移動していなければ)
cd umi-astro

# 2. 依存関係をインストールする
# package.json に基づいて、すべての必要なモジュールをダウンロードします。
npm install

# 3. インストールが完了したら、開発サーバーを起動
npm run dev

run devで

% npm run dev

> umi-astro@0.0.1 dev
> astro dev

23:44:58 [types] Generated 1ms
23:44:58 [content] Syncing content
23:44:58 [content] Synced content

 astro  v5.15.1 ready in 363 ms

┃ Local    http://localhost:4321/
┃ Network  use --host to expose

23:44:58 watching for file changes...
23:45:08 [200] / 16ms

http://localhost:4321/にアクセスして確認OK

astro@michel5.15.1 versionは 5.15.1ですね

検索やaiで調べるときはversion指定しないと古い情報も多いです

Tailwind追加

cd umi-astroで tailwind 追加

 % npx astro add tailwind
Ne % npx astro add tailwind
✔ Resolving packages...

  Astro will run the following command:
  If you skip this step, you can always run it yourself later

 ╭──────────────────────────────────────────────────────╮
 │ npm i @tailwindcss/vite@^4.1.16 tailwindcss@^4.1.16  │
 ╰──────────────────────────────────────────────────────╯

✔ Continue? … yes
⠙ Installing dependencies...
 Astro will scaffold ./src/styles/global.css.

✔ Continue? … yes

  Astro will make the following changes to your config file:

 ╭ astro.config.mjs ─────────────────────────────╮
 │ // @ts-check                                  │
 │ import { defineConfig } from 'astro/config';  │
 │                                               │
 │ import tailwindcss from '@tailwindcss/vite';  │
 │                                               │
 │ // https://astro.build/config                 │
 │ export default defineConfig({                 │
 │   vite: {                                     │
 │     plugins: [tailwindcss()]                  │
 │   }                                           │
 │ });                                           │
 ╰───────────────────────────────────────────────╯

✔ Continue? … yes
  
   success  Added the following integration to your project:
  - tailwind
  
   action required  You must import your Tailwind stylesheet, e.g. in a shared layout:

 ╭ src/layouts/Layout.astro ──────╮
 │ ---                            │
 │ import '../styles/global.css'  │
 │ ---                            │
 ╰────────────────────────────────╯

とりあえずインストールOK

Astro i18n 多言語構成

astro.config.mjsの編集

// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  // ------------------------------
  i18n: {
      // ...i18n の設定
      defaultLocale: 'en',
      locales: ['en', 'ja'],
      routing: {
          prefixDefaultLocale: false,
      },
  },

  vite: {
    plugins: [tailwindcss()],
  },
});  

tailwindはvite経由で◎


翻訳ファイルの作成とルーティング

翻訳ディレクトリーとファイル作成

mkdir src/i18n
touch src/i18n/ui.ts # すべての翻訳を統合するファイル

common.js

export const common_translations = {
  en: {
    'nav.home': 'Home',
    'nav.about': 'About',
    'language.en': 'English',
    'language.ja': 'Japanese',
    'footer.note': 'All rights reserved.',
  },
  ja: {
    'nav.home': 'ホーム',
    'nav.about': '会社概要',
    'language.en': '英語',
    'language.ja': '日本語',
    'footer.note': '無断転載を禁じます。',
  },
};

home

export const home_translations = {
  en: {
    'page.title': 'Welcome to Our International Site',
    'page.description': 'This is the English version.',
  },
  ja: {
    'page.title': '国際サイトへようこそ',
    'page.description': 'こちらは日本語版です。',
  },
};

ui.ts ( 統合 , ページごと

// src/i18n/ui.ts

import type { Translations } from './dictionary'; //  ⭐️  新しく作成した型をインポート

/**
 * デフォルト言語を定義します。
 * 'en' がデフォルト言語(プレフィックスなしの URL 例: /about)
 */
export const defaultLang = 'en';

/**
 * 翻訳辞書。
 * すべての翻訳キーとその値を言語ごとに定義します。
 */
// ui オブジェクトに Translations 型を適用
export const ui: Translations = {
en: {
  // --- アプリ共通情報 ---
  'app.name': 'umi Retro Snap',
  'app.tagline': 'film look camera with live filters – no editing required',
  'app.download': 'Download on the App Store',
  'country.japan': 'Japan',
},
ja : {
},
};
// src/i18n/utils.ts

import { ui, defaultLang } from './ui';
import type { AvailableKeys } from './dictionary'; //  ⭐️  新しくインポート

// 1. 翻訳関数 (t) の生成
export function getI18nTranslator(locale: keyof typeof ui) {
  const targetLocale = ui.hasOwnProperty(locale) ? locale : defaultLang;

  // 翻訳関数 t は AvailableKeys 型のキーのみを受け付ける
  return function t(key: AvailableKeys) {

    // ⬇️ この行でエラーが発生していました
    // ロケールが見つからない場合、あるいはキーが存在しない場合に備えて、
    // ターゲットロケールの翻訳、デフォルトロケールの翻訳、最後にキー名自体を返す

    // ui[targetLocale] が未定義になる可能性は排除済み
    const translation = ui[targetLocale]?.[key] || ui[defaultLang]?.[key];

    // どの辞書にもキーが存在しない場合 (本来は型で防ぐべきだが、安全策として)
    return translation || key;
  }
}

// 2. 言語切り替えURLの生成 (変更なし)
export function getLocaleUrl(targetLocale: keyof typeof ui, currentPath: string): string {
  // ... 以前のロジック ...
  const pathWithoutLocale = currentPath.replace(/^\/(ja|en)/, '');

  if (targetLocale === defaultLang) {
    return pathWithoutLocale || '/';
  } else {
    return `/${targetLocale}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`;
  }
}

なんか辞書でエラーになるのでこんな感じに。。。

言語スイッチャーコンポーネントの作成

src/components/LanguageSwitcher.astro

---
// ✅  カスタムユーティリティのみをインポート
import { getI18nTranslator, getLocaleUrl } from '../i18n/utils';
import { ui } from '../i18n/ui';

// ✅  ロケールは 'Astro.currentLocale' を使用 (これは安定しているはず)
const locale = Astro.currentLocale as keyof typeof ui;
const locales = ['en', 'ja'] as const;
const t = getI18nTranslator(locale);

// URL生成のための現在のパスを取得
const currentPath = Astro.url.pathname;
---

<div class="flex space-x-2">
    {locales.map((lang) => (
    <a
        // ✅  カスタムヘルパーで URL を生成
        href={getLocaleUrl(lang, currentPath)}
        class={`px-3 py-1 rounded-full text-sm transition-colors ${
        locale === lang
        ? 'bg-white text-blue-600 font-bold'
        : 'bg-blue-500 hover:bg-blue-400 text-white'
        }`}
        >
        {t(`language.${lang}`)}
    </a>
        ))}
</div>

ダイナミックルーティング

/ /ja/ /fr/ みたいに切り替える方法です

記法機能名用途
[...lang]Rest パラメータを持つダイナミックルーティング任意の深さのパスセグメントを、単一のパラメータとしてキャッチします。多言語(i18n)やブログのカスタムURLなどに使用されます。
[slug]ダイナミックルーティング単一のパスセグメント(例: /posts/hello-worldhello-world)をパラメータとしてキャッチします。

ほー。

Rest パラメータとは?
残余引数 !!!!なにそれ^^;
残りのパス(URLセグメント)をまとめて捕捉するパラメータ」実働はわかるのですが、言葉での説明は難しいですね

今回は […lang]で lang配列を処理できるのですが

slugもuriの /slug/slug/ というのはわかりますが日本語でなんなの?と
uriにつかうキーワード識別子という感じですがわかってはいても知らない人への説明は難しいものですね

src/pages/[…lang]/index.astro

---
// ===============================================
// 1. Astro API と ユーティリティのインポート
// ===============================================
import Layout from '../../layouts/Layout.astro'; // 相対パスを修正
import LanguageSwitcher from '../../components/LanguageSwitcher.astro';
import { getI18nTranslator } from '../../i18n/utils';
import { ui } from '../../i18n/ui';

// ===============================================
// 2. ダイナミックルーティングとロケールの取得
// ===============================================
export function getStaticPaths() {
const locales = ['en', 'ja'];
return locales.map(lang => ({
params: { lang: lang === 'en' ? undefined : lang }, // デフォルト言語 'en' はプレフィックスなし
props: { locale: lang },
}));
}

// Props から locale を受け取る
const { locale } = Astro.props;
const t = getI18nTranslator(locale);
<Layout title={t('page.title')} locale={locale} showHero={true}>


    <main class="text-gray-800">

        <section id="about" class="py-16 max-w-4xl mx-auto px-4">
            <h2 class="text-3xl font-bold text-gray-900 mb-8 border-b pb-2">
                {t('section.about.title')}
            </h2>
            <div class="grid md:grid-cols-2 gap-12 items-center">
                <div class="bg-gray-200 h-80 rounded-xl flex items-center justify-center">
                </div>

                <div>
                    <p class="text-lg mb-6 leading-relaxed">
                              {t('section.about.p1')}
                    </p>

                    <ul class="space-y-4">
                        <li class="flex items-start">
                            <span class="text-blue-500 mr-3 text-xl">✅ </span>
                            <div>
                                <h3 class="font-semibold text-lg">{t('feature.1.title')}</h3>
                                <p class="text-gray-600 text-sm">{t('feature.1.desc')}</p>
                            </div>
                        </li>
                        <li class="flex items-start">
                            <span class="text-blue-500 mr-3 text-xl">�️ </span>
                            <div>
                                <h3 class="font-semibold text-lg">{t('feature.2.title')}</h3>
                                <p class="text-gray-600 text-sm">{t('feature.2.desc')}</p>
                            </div>
                        </li>
                    </ul>
                </div>
            </div>
        </section>
    </main>

</Layout>

{t(‘abc.def’)} が ui.tsに入っているパラメータです

これでテンプレート ( index.astro ) を1ページ分作ると あとは翻訳を入れるだけで すべての翻訳ページが生成できます! OK^^

最初の astro使用記事はこちら

【 Astro / TailWind CSS 】静的サイトジェネレーター SSG 始めます!
Astro - TailWind CSS -PHPやrubyなどはだいぶ古くなってしまったようで、通常のサイト作成はSSGが主流になっているようですね!?ということで Astro x TailWind CSSでサイ...

Astro –
https://astro.build/

TailWind CSS –
https://tailwindcss.com/

お気軽にコメントください!

スパム対応のためコメント認証に数日かかることがありますが、お気軽にコメントいただけると嬉しいです^^

コメント

タイトルとURLをコピーしました