Next.js: 国際化 & ミドルウェア
最終更新:2026-08-26
国際化は単なる翻訳ではありません。ルーティング、ロケール検出、RTL レイアウトを含む体系的なエンジニアリングプロセスです。
1. 学習目標
next-intl国際化フレームワークのインストールと統合- ミドルウェアルーティングポリシー(
Accept-Language検出 + Cookie 永続化 + URL 書き換え) - 多言語辞書管理(
en.json/ar.json構造化翻訳) generateStaticParamsで多言語静的ページを事前生成- RTL レイアウトサポート(アラビア語のテキスト方向適応)
- 日付、数値、通貨の国際フォーマット
2. 国際プロダクトマネージャーの実話
(1) 課題: 中東のユーザーから 47 件のネガティブレビュー
Alice の TaskFlow がローンチされた後、サウジアラビアとアラブ首長国連邦のユーザーから多数の苦情が寄せられました:
「ダッシュボードはまだ英語で、日付形式 07/06/2026 は彼らにとって意味がなく(サウジアラビアでは 06/07/2026 を使用)、数字 1,234.56 はアラブ諸国では正しく表示されません。最悪なのは、ページが右から左にスクロールし、レイアウト全体が反転していることです。」
| 問題 | ユーザーフィードバック | 影響を受けたユーザー数 |
|---|---|---|
| アラビア語に翻訳されていない | 「メニューが理解できない」 | 47 件のネガティブレビュー |
| 日付形式が間違っている | 「日付が逆になっている」 | 32 件の苦情 |
| レイアウトが RTL に対応していない | 「ボタンが右側にある」 | 28 件の苦情 |
| 通貨記号のエラー | 「価格が USD で表示されている」 | 15 件の苦情 |
(2) next-intl + ミドルウェアの解決策
next-intlで国際化フレームワークを提供し、ミドルウェアがブラウザの言語に基づいて自動的に切り替えます。
// middleware.ts — 国際化ルーティングのコア
import createMiddleware from 'next-intl/middleware'
export default createMiddleware({
locales: ['zh', 'en', 'ja', 'ar'],
defaultLocale: 'zh',
localePrefix: 'always' // URL には常に地域プレフィックスを含める
})
export const config = {
matcher: ['/((?!_next|api|favicon.ico).*)']
}
(3) 効果
| 観点 | 再設計前 | 再設計後 |
|---|---|---|
| 対応言語 | 中国語のみ | 中国語、英語、日本語、アラビア語の 4 言語 |
| 言語検出 | 手動選択 | ブラウザ言語の自動検出 |
| RTL サポート | ❌ 非対応 | ✅ 完全ミラーレイアウト |
| 日付/数値 | 中国語形式をハードコード | Intl API で自動フォーマット |
| サウジアラビアユーザー NPS | 32 | 78 |
3. next-intl のインストールとプロジェクト構造
(1) インストールとディレクトリ構造
npm install next-intl
graph TB
A[next-intl] --> B[i18n/request.ts<br/>サーバー設定]
A --> C[messages/<br/>翻訳辞書]
C --> D[en.json<br/>英語]
C --> E[zh.json<br/>中国語]
C --> F[ja.json<br/>日本語]
C --> G[ar.json<br/>アラビア語]
A --> H[middleware.ts<br/>言語検出 + 書き換え]
A --> I[app/[locale]/<br/>動的ルーティングセグメント]
style A fill:#cce5ff
style H fill:#d4edda
style I fill:#fff3cd
| ファイル | 目的 |
|---|---|
i18n/request.ts |
サーバーサイド i18n 設定のエントリポイント |
messages/en.json |
英語辞書 |
messages/ar.json |
アラビア語辞書(RTL マークアップ付き) |
middleware.ts |
自動言語検出 + URL 書き換え |
app/[locale]/ |
動的地域ルーティングセグメント |
(2) 初期設定
// i18n/request.ts — next-intl サーバー設定
import { getRequestConfig } from 'next-intl/server'
import { hasLocale } from 'next-intl'
import { routing } from './routing'
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default
}
})
// i18n/routing.ts — 共有ルーター設定
import { defineRouting } from 'next-intl/routing'
export const routing = defineRouting({
locales: ['zh', 'en', 'ja', 'ar'],
defaultLocale: 'zh',
localeDetection: true,
localePrefix: 'as-needed' // デフォルト言語のプレフィックスを省略
})
▶ サンプル: 完全なミドルウェア設定
Output:
Exports: routing.
// middleware.ts — 言語検出 + Cookie 永続化
import createMiddleware from 'next-intl/middleware'
import { NextRequest, NextResponse } from 'next/server'
const intlMiddleware = createMiddleware({
locales: ['zh', 'en', 'ja', 'ar'],
defaultLocale: 'zh',
localeDetection: true,
localePrefix: 'always'
})
export default function middleware(req: NextRequest) {
const { pathname } = req.nextUrl
// API / _next は処理しない
if (pathname.startsWith('/api') || pathname.startsWith('/_next') || pathname === '/favicon.ico') {
return NextResponse.next()
}
return intlMiddleware(req)
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
Output:
Middleware processes requests before they reach route handlers.
4. ミドルウェアルーティングポリシー
(1) 3 つのルーティングモード
graph TB
A[ユーザーアクセス] --> B{ミドルウェア検証}
B --> C[Cookie を検査<br/>next-intl-locale]
B --> D[Accept-Language を検査<br/>HTTP ヘッダー]
B --> E[デフォルト言語]
C --> F[選択済み → 使用]
D --> G[初回訪問 → ブラウザ言語を検出]
E --> H[一致なし → デフォルト言語]
F --> I[URL 書き換え /{locale}/...]
G --> I
H --> I
I --> J[Cookie を書き込み<br/>永続化]
style B fill:#fff3cd
style I fill:#d4edda
style J fill:#cce5ff
| ルーティングモード | URL 形式 | localePrefix |
適用シナリオ |
|---|---|---|---|
| 常にプレフィックス | /en/about / /ar/about |
'always' |
全言語を均等に表示 |
| オンデマンドプレフィックス | /about (デフォルト) / /en/about |
'as-needed' |
デフォルト言語の SEO フレンドリー |
| プレフィックスなし | /about |
'never' |
Cookie 駆動(非推奨) |
(2) Accept-Language 検出 + Cookie 永続化
// middleware.ts — 検出ロジックの手動制御
import { NextRequest, NextResponse } from 'next/server'
import { routing } from './i18n/routing'
const COOKIE_NAME = 'NEXT_LOCALE'
export default function middleware(req: NextRequest) {
const { pathname } = req.nextUrl
const cookieLocale = req.cookies.get(COOKIE_NAME)?.value
const acceptLanguage = req.headers.get('Accept-Language')?.split(',')[0]?.split('-')[0]
const browserLocale = acceptLanguage && routing.locales.includes(acceptLanguage as any) ? acceptLanguage : null
// 言語のマッチング
const matchedLocale = cookieLocale || browserLocale || routing.defaultLocale
// パスに既に言語プレフィックスがある
const pathLocale = routing.locales.find((l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`)
if (pathLocale) return NextResponse.next()
// URL を書き換えて Cookie を設定
const url = new URL(`/${matchedLocale}${pathname}`, req.url)
const res = NextResponse.rewrite(url)
res.cookies.set(COOKIE_NAME, matchedLocale, { maxAge: 60 * 60 * 24 * 365 })
return res
}
export const config = { matcher: ['/((?!api|_next|favicon.ico).*)'] }
▶ サンプル: 言語切り替えコンポーネント
Output:
Renders the middleware component UI.
// components/LocaleSwitcher.tsx
'use client'
import { useLocale } from 'next-intl'
import { usePathname, useRouter } from '@/i18n/routing'
import { useTransition } from 'react'
const locales = [
{ code: 'zh', label: '中国語' },
{ code: 'en', label: 'English' },
{ code: 'ja', label: '日本語' },
{ code: 'ar', label: 'العربية' }
]
export default function LocaleSwitcher() {
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const [isPending, startTransition] = useTransition()
const switchLocale = (nextLocale: string) => {
startTransition(() => {
router.replace(pathname, { locale: nextLocale })
})
}
return (
<select
value={locale}
onChange={(e) => switchLocale(e.target.value)}
disabled={isPending}
style={{ padding: '0.25rem 0.5rem' }}
>
{locales.map((l) => (
<option key={l.code} value={l.code}>{l.label}</option>
))}
</select>
)
}
Output:
Renders the ▶ サンプル: 言語切り替えコンポーネント component UI as described in the section.
5. 多言語辞書管理
(1) 辞書ファイル構造
// messages/zh.json
{
"nav": {
"home": "ホーム",
"dashboard": "ダッシュボード",
"projects": "プロジェクト",
"settings": "設定",
"signIn": "ログイン",
"signOut": "終了"
},
"home": {
"title": "TaskFlow - チームコラボレーションプラットフォーム",
"subtitle": "10,000 以上のチームの効率的なコラボレーションを支援します",
"cta": "無料で始める"
},
"dashboard": {
"welcome": "おかえりなさい、{name}",
"stats": {
"members": "{count} 名のメンバー",
"projects": "{count} プロジェクト",
"tasks": "{count} タスク"
},
"activity": "最近のアクティビティ"
},
"common": {
"loading": "読み込み中...",
"error": "エラーが発生しました",
"save": "保存",
"cancel": "キャンセル",
"delete": "削除",
"confirm": "確認"
}
}
// messages/ar.json
{
"nav": {
"home": "الرئيسية",
"dashboard": "لوحة التحكم",
"projects": "المشاريع",
"settings": "الإعدادات",
"signIn": "تسجيل الدخول",
"signOut": "تسجيل الخروج"
},
"home": {
"title": "TaskFlow - منصة التعاون الجماعي",
"subtitle": "نساعد أكثر من 10,000 فريق على العمل بكفاءة",
"cta": "ابدأ مجاناً"
},
"dashboard": {
"welcome": "مرحباً بعودتك، {name}",
"stats": {
"members": "{count} أعضاء",
"projects": "{count} مشروعاً",
"tasks": "{count} مهمة"
},
"activity": "النشاطات الأخيرة"
},
"common": {
"loading": "جارٍ التحميل...",
"error": "حدث خطأ",
"save": "حفظ",
"cancel": "إلغاء",
"delete": "حذف",
"confirm": "تأكيد"
}
}
(2) コンポーネントでの翻訳の使用
// サーバーコンポーネント: t() 関数を使用
import { getTranslations } from 'next-intl/server'
export default async function HomePage() {
const t = await getTranslations('home')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('subtitle')}</p>
<button>{t('cta')}</button>
</div>
)
}
// クライアントコンポーネント: useTranslations フック
'use client'
import { useTranslations } from 'next-intl'
export default function DashboardHeader({ name }: { name: string }) {
const t = useTranslations('dashboard')
return (
<h1>{t('welcome', { name })}</h1>
)
}
おかえりなさい、Alice
💻 出力 (アラビア語): مرحباً بعودتك، Alice
▶ サンプル: 複数形を含む翻訳
// 辞書
{
"tasks": {
"remaining": "{count, plural, =0 {タスクなし} one {# 件のタスク} other {# 件のタスク}}"
}
}
import { useTranslations } from 'next-intl'
export default function TaskCount({ count }: { count: number }) {
const t = useTranslations('tasks')
return <span>{t('remaining', { count })}</span>
}
Output:
Renders the TaskCount component UI.
タスクなし
💻 出力 (count=1): 1 件のタスク
💻 出力 (count=5): 5 件のタスク
next-intl は ICU MessageFormat 構文を使用し、複数形、選択肢、数値フォーマットなどの高度な機能をサポートしています。
6. generateStaticParams: 多言語ページの事前生成
(1) 静的事前生成の原理
graph LR
A[generateStaticParams] --> B[ロケールリストを返却]
B --> C[zh: /zh/blog/post-1]
B --> D[en: /en/blog/post-1]
B --> E[ja: /ja/blog/post-1]
B --> F[ar: /ar/blog/post-1]
C --> G[ビルド時に生成: 4 つの HTML ファイル]
D --> G
E --> G
F --> G
style A fill:#cce5ff
style G fill:#d4edda
| 言語 | HTML ファイル | パス |
|---|---|---|
| 中国語 | out/zh/blog/post-1.html |
/zh/blog/post-1 |
| 英語 | out/en/blog/post-1.html |
/en/blog/post-1 |
| 日本語 | out/ja/blog/post-1.html |
/ja/blog/post-1 |
| アラビア語 | out/ar/blog/post-1.html |
/ar/blog/post-1 |
(2) ルートレイアウトの "locale" パラメータ
// app/[locale]/layout.tsx — ルートレイアウトが locale パラメータを受け取る
import { NextIntlClientProvider } from 'next-intl'
import { getMessages, getTranslations } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { routing } from '@/i18n/routing'
type Props = { children: React.ReactNode; params: Promise<{ locale: string }> }
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params
// locale が有効か検証
if (!routing.locales.includes(locale as any)) notFound()
const messages = await getMessages()
return (
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
▶ サンプル: 多言語ブログ記事の静的生成
Output:
Renders the LocaleLayout component UI.
// app/[locale]/blog/[slug]/page.tsx — 多言語 SSG
import { getTranslations } from 'next-intl/server'
import { routing } from '@/i18n/routing'
type Props = { params: Promise<{ locale: string; slug: string }> }
// 全言語の記事ページを事前生成
export async function generateStaticParams() {
const posts = await fetch('https://api.taskflow.io/blog/posts').then(r => r.json())
return routing.locales.flatMap((locale) =>
posts.map((post: { slug: string }) => ({ locale, slug: post.slug }))
)
}
export async function generateMetadata({ params }: Props) {
const { locale, slug } = await params
const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())
return {
title: locale === 'en' ? post.title : post.titleLocalized[locale],
alternates: {
languages: Object.fromEntries(
routing.locales.map((l) => [l, `https://taskflow.io/${l}/blog/${slug}`])
)
}
}
}
export default async function BlogPostPage({ params }: Props) {
const { locale, slug } = await params
const t = await getTranslations('blog')
return (
<article>
<h1>{t('title')}</h1>
<p>Locale: {locale} | Slug: {slug}</p>
</article>
)
}
Output:
Renders: Page with "{t('title')}" heading and associated content.
7. RTL レイアウトサポート
(1) RTL と LTR の違い
graph TB
subgraph LTR レイアウト
A1[ロゴ<br/>左揃え] --> A2[ナビゲーションバー<br/>左から右]
A2 --> A3[サイドバー<br/>左揃え]
A3 --> A4[コンテンツ<br/>右側に配置]
end
subgraph RTL レイアウト
B1[ロゴ<br/>右揃え] --> B2[ナビゲーションバー<br/>右から左]
B2 --> B3[サイドバー<br/>右揃え]
B3 --> B4[コンテンツ<br/>左側に配置]
end
style B1 fill:#d4edda
style B2 fill:#d4edda
style B3 fill:#d4edda
| 観点 | LTR (中国語/英語/日本語) | RTL (アラビア語) |
|---|---|---|
| テキスト方向 | 左から右 | 右から左 |
| ナビゲーションバー | ロゴ左 → メニュー右 | ロゴ右 → メニュー左 |
| サイドバー | 左 | 右 |
| 矢印アイコン | → 進む |
← 進む |
| カードマージン | margin-left: auto |
margin-right: auto |
| フォーム送信ボタン | 右下 | 左下 |
(2) Tailwind CSS RTL サポート
// app/[locale]/layout.tsx — RTL 検出と CSS クラス名
export default async function LocaleLayout({ children, params }: {
children: React.ReactNode
params: Promise<{ locale: string }>
}) {
const { locale } = await params
const isRtl = locale === 'ar'
return (
<html lang={locale} dir={isRtl ? 'rtl' : 'ltr'}>
<body className={isRtl ? 'rtl' : 'ltr'}>
{children}
</body>
</html>
)
}
/* app/globals.css — RTL 基本スタイル */
.ltr { direction: ltr; }
.rtl { direction: rtl; }
/* ナビゲーションバーのミラーリング */
.rtl .nav-items { flex-direction: row-reverse; }
.rtl .sidebar { right: 0; left: auto; }
.rtl .main-content { margin-right: 260px; margin-left: 0; }
/* アイコンのミラーリング */
.rtl .icon-arrow::before { content: '←'; }
.ltr .icon-arrow::before { content: '→'; }
/* スペーシングの反転 */
.rtl .ml-auto { margin-right: auto; margin-left: 0; }
.rtl .mr-2 { margin-left: 0.5rem; margin-right: 0; }
▶ サンプル: RTL 最適化ダッシュボードレイアウト
Output:
CSS styles applied for: .nav-items, .mr-2, .sidebar, .rtl, before, .main-content
// components/DashboardLayout.tsx — RTL 互換レイアウト
export default function DashboardLayout({
sidebar,
children,
locale
}: {
sidebar: React.ReactNode
children: React.ReactNode
locale: string
}) {
const isRtl = locale === 'ar'
return (
<div style={{
display: 'flex',
flexDirection: isRtl ? 'row-reverse' : 'row',
minHeight: '100vh'
}}>
{/* サイドバー: LTR では左、RTL では右 */}
<aside style={{
width: 260,
borderRight: isRtl ? 'none' : '1px solid #e5e7eb',
borderLeft: isRtl ? '1px solid #e5e7eb' : 'none'
}}>
{sidebar}
</aside>
{/* メインコンテンツ領域 */}
<main style={{
flex: 1,
padding: '2rem',
textAlign: isRtl ? 'right' : 'left'
}}>
{children}
</main>
</div>
)
}
Output:
Renders: Dashboard layout with dark sidebar (Dashboard/Projects/Settings links) + main content area.
direction: rtl だけではありません。すべての flex 方向、マージン/パディング、アイコン矢印、テキスト配置もミラーリングする必要があります。
8. 完全なサンプル: 4 言語対応公式サイト + RTL ダッシュボード
// app/[locale]/layout.tsx — 国際化ルートレイアウト
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
import { routing } from '@/i18n/routing'
import { notFound } from 'next/navigation'
import LocaleSwitcher from '@/components/LocaleSwitcher'
import type { Metadata } from 'next'
type Props = { children: React.ReactNode; params: Promise<{ locale: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params
return {
title: 'TaskFlow',
alternates: {
canonical: `https://taskflow.io/${locale}`,
languages: Object.fromEntries(
routing.locales.map((l) => [l, `https://taskflow.io/${l}`])
)
}
}
}
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params
if (!routing.locales.includes(locale as any)) notFound()
const messages = await getMessages()
const isRtl = locale === 'ar'
return (
<html lang={locale} dir={isRtl ? 'rtl' : 'ltr'}>
<head>
{/* RTL CSS の読み込み */}
{isRtl && <link rel="stylesheet" href="/rtl.css" />}
</head>
<body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>
<NextIntlClientProvider messages={messages}>
<nav style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem 2rem',
borderBottom: '1px solid #e5e7eb',
flexDirection: isRtl ? 'row-reverse' : 'row'
}}>
<strong>TaskFlow</strong>
<LocaleSwitcher />
</nav>
<main style={{ padding: '2rem' }}>{children}</main>
</NextIntlClientProvider>
</body>
</html>
)
}
// app/[locale]/dashboard/page.tsx — RTL ダッシュボード
import { getTranslations } from 'next-intl/server'
type Props = { params: Promise<{ locale: string }> }
export default async function DashboardPage({ params }: Props) {
const { locale } = await params
const t = await getTranslations('dashboard')
const isRtl = locale === 'ar'
return (
<div>
<h1 style={{ textAlign: isRtl ? 'right' : 'left' }}>{t('welcome', { name: 'Alice' })}</h1>
<div style={{
display: 'flex',
gap: '1rem',
flexDirection: isRtl ? 'row-reverse' : 'row'
}}>
<div style={{ flex: 1, padding: '1.5rem', background: '#f0f9ff', borderRadius: 8 }}>
<h3>{t('stats.members', { count: 12 })}</h3>
</div>
<div style={{ flex: 1, padding: '1.5rem', background: '#fef3c7', borderRadius: 8 }}>
<h3>{t('stats.projects', { count: 45 })}</h3>
</div>
<div style={{ flex: 1, padding: '1.5rem', background: '#ecfdf5', borderRadius: 8 }}>
<h3>{t('stats.tasks', { count: 230 })}</h3>
</div>
</div>
<h2 style={{ textAlign: isRtl ? 'right' : 'left' }}>{t('activity')}</h2>
<div style={{
padding: '1rem',
border: '1px solid #e5e7eb',
borderRadius: 8,
textAlign: isRtl ? 'right' : 'left'
}}>
<p>Alice がプロジェクト「ウェブサイトリデザイン」を作成しました</p>
<p>Bob がタスク #47 を完了しました</p>
<p>Charlie が組織に参加しました</p>
</div>
</div>
)
}
# 出力結果 (中国語):
TaskFlow
[言語を選択: 中国語 ▼]
おかえりなさい、Alice
[12 名のメンバー] [45 プロジェクト] [230 タスク]
最近のアクティビティ
Alice がプロジェクト「ウェブサイトリデザイン」を作成しました
Bob がタスク #47 を完了しました
Charlie が組織に参加しました
# 出力結果 (アラビア語):
TaskFlow
[اختيار اللغة: العربية ▼]
مرحباً بعودتك، Alice
[12 أعضاء] [45 مشروعاً] [230 مهمة]
النشاطات الأخيرة
أليس قامت بإنشاء مشروع "إعادة تصميم الموقع"
بوب أكمل المهمة رقم 47
تشارلي انضم إلى المنظمة
❓ よくある質問
next-intl と next-international のどちらを選ぶべきですか?next-intl はより大きなコミュニティ(5k⭐)とより多くの機能(複数形、選択肢、数値フォーマット、RTL 検出)を持っています。next-international はより軽量です。このチュートリアルでは next-intl を使用します。rewrite はブラウザのアドレスバーの内容を変更せずに内部的に /{locale}/path にマッピングするため、SEO とユーザー体験に優れています。redirect は URL を変更するため、ユーザーが共有したリンクが壊れる可能性があります。direction: rtl に加えて、flex 方向のミラーリング(flex-direction: row-reverse)、マージンとパディングの方向調整、矢印アイコンの反転、left/right の代わりに text-align: start/end を使用する必要があります。globals.css で [dir="rtl"] セレクタを使用して統一的に処理することをお勧めします。generateStaticParams は各言語に個別の HTML ファイルを生成しますか?/zh/blog/post-1、/en/blog/post-1、/ar/blog/post-1 は 3 つの個別の HTML ファイルです。各ファイルには対応する言語のコンテンツのみが含まれ、最速の読み込み速度を保証します。{name} で変数を挿入、{count, plural, =0 {なし} one {# 件のタスク} other {# 件のタスク}} で複数形を処理します。next-intl の t() および useTranslations() フックがこれらのテンプレートを自動的に解析します。📖 まとめ
next-intlは包括的な国際化ソリューションを提供します: ミドルウェア(言語検出)+ 辞書管理 + RTL サポート- ミドルウェアは優先順位に従ってチェックします: Cookie → Accept-Language → デフォルト言語。結果は Cookie に書き込まれて永続化されます
- 辞書はネストされた JSON 構造で管理され、複数形、変数、選択肢のための ICU MessageFormat をサポートします
generateStaticParamsは各言語に個別の HTML を事前生成します。SSG モードではランタイムオーバーヘッドがゼロです- RTL サポートには
dirプロパティ + CSS ミラーリング(flex 方向 / マージン / パディング / アイコン)が必要です - 日付、数値、通貨は JavaScript の
IntlAPI を使用して自動フォーマットされ、追加のライブラリは不要です
📝 練習問題
-
基本問題 (⭐): 既存のプロジェクトに
next-intlをインストールし、中国語と英語の辞書を設定し、言語切り替えコンポーネントを実装し、t()を使用してページにナビゲーションバーのテキストをレンダリングしてください。 -
応用問題 (⭐⭐): 完全な多言語 SSG ブログを実装してください:
generateStaticParamsで 4 言語の記事ページを事前生成し、各記事がビルドプロセス中に対応する言語の辞書を読み込むようにし、生成された HTML ファイルの構造を検証します。 -
発展問題 (⭐⭐⭐): RTL をサポートするダッシュボードページを構築してください: アラビア語と英語の両方に対応し、Tailwind CSS の
rtl:プレフィックスとflex-directionミラーリングを使用して、サイドバー、ナビゲーション、統計カードの完全なレイアウト適応を実装します。dir="rtl"ですべての要素が正しくミラーリングされることを検証してください。